Compare commits
91 Commits
profunctor
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8e2562a50 | ||
|
|
d8ede5b259 | ||
|
|
0d16c869f9 | ||
|
|
fba97c405b | ||
|
|
91c45de4d1 | ||
|
|
28b33fee2d | ||
|
|
30f1cb8bd2 | ||
|
|
e956f26403 | ||
|
|
b1fffe2561 | ||
|
|
d7494a102f | ||
|
|
fa682cbfdc | ||
|
|
11ced47370 | ||
|
|
518423ef9e | ||
|
|
12b9f0e4a0 | ||
|
|
df9443c763 | ||
|
|
e20a15832b | ||
|
|
20d0071a24 | ||
|
|
4aa89dcdaa | ||
|
|
b9ea39ffa3 | ||
|
|
d17193baae | ||
|
|
36cf1917d8 | ||
|
|
d2604f80cb | ||
|
|
f6020efa00 | ||
|
|
8f0861d52e | ||
|
|
06b5ffcd40 | ||
|
|
7206b17175 | ||
|
|
4cea6fee1f | ||
|
|
56787f573c | ||
|
|
7fdd984470 | ||
|
|
4c5446afea | ||
|
|
372cd4b843 | ||
|
|
84ce755f19 | ||
|
|
f9a8a7d992 | ||
|
|
b0d26a8691 | ||
|
|
e80f7cdd83 | ||
|
|
63a5242d07 | ||
|
|
3d32e8017e | ||
|
|
81b5598ed1 | ||
|
|
b747d71d75 | ||
|
|
53f9ebeea0 | ||
|
|
cb5be2ab25 | ||
|
|
a3d4c36bfa | ||
|
|
17b1473359 | ||
|
|
f115e7798b | ||
|
|
4f3e83a908 | ||
|
|
add35c3fc1 | ||
|
|
c01dce8eb2 | ||
|
|
0427fd82e2 | ||
|
|
eeaa05d2a2 | ||
|
|
8c0faf9ae2 | ||
|
|
2d5ae3851a | ||
|
|
50ffb67738 | ||
|
|
e3f2eb8ccf | ||
|
|
900f6a2e18 | ||
|
|
6300c03a5f | ||
|
|
16457188fe | ||
|
|
7e002f9d5b | ||
|
|
24a2c1d142 | ||
|
|
11f9a10268 | ||
|
|
59318ccb26 | ||
|
|
f07bb06e1b | ||
|
|
72ea18ba5e | ||
|
|
13b0f64b69 | ||
|
|
3529a72950 | ||
|
|
3f4d0fb5cd | ||
|
|
f62d10b75c | ||
|
|
4886ad9ff0 | ||
|
|
01a75dc318 | ||
|
|
21f6767a44 | ||
|
|
44b55d2df4 | ||
|
|
a0b4b1aa7e | ||
|
|
45c961fdd1 | ||
|
|
83e069d1b6 | ||
|
|
fb6064b79f | ||
|
|
03e9e3734b | ||
|
|
fca7d72085 | ||
|
|
432ab8d193 | ||
|
|
88b2704951 | ||
|
|
bfb8e59c09 | ||
|
|
76cb112361 | ||
|
|
31c423ad1a | ||
|
|
cb9d9091b8 | ||
|
|
3a5e731d29 | ||
|
|
7482a66b3e | ||
|
|
c188d728bb | ||
|
|
c646c467c9 | ||
|
|
e0a0f66a43 | ||
|
|
7919b2c5ac | ||
|
|
7aa60cf7d1 | ||
|
|
6b007f8a7e | ||
|
|
dccacf0d75 |
26
.gitignore
vendored
26
.gitignore
vendored
@ -5,8 +5,6 @@ cabal.config
|
||||
cabal.sandbox.config
|
||||
*.chi
|
||||
*.chs.h
|
||||
config/client_session_key.aes
|
||||
playground/
|
||||
dist*
|
||||
.DS_Store
|
||||
*.dyn_hi
|
||||
@ -19,23 +17,23 @@ dist*
|
||||
*.o
|
||||
*.prof
|
||||
*.sqlite3
|
||||
untracked/
|
||||
uploads/
|
||||
static/combined/
|
||||
static/tmp/
|
||||
*.swp
|
||||
.virtualenv
|
||||
.stack-work/
|
||||
yesod-devel/
|
||||
tmp/
|
||||
config/client_session_key.aes
|
||||
playground/auth.txt
|
||||
**/*.dump-hi
|
||||
tags
|
||||
TAGS
|
||||
colonnade/ex1.hs
|
||||
colonnade/result
|
||||
|
||||
docs/db/unthreat
|
||||
ex1.hs
|
||||
|
||||
geolite-csv/data/large
|
||||
geolite-lmdb/data/large
|
||||
reflex-dom-colonnade/result
|
||||
siphon-0.8.0-docs.tar.gz
|
||||
siphon-0.8.0-docs/
|
||||
.ghc.environment.*
|
||||
example
|
||||
example.hs
|
||||
example1
|
||||
example1.hs
|
||||
client_session_key.aes
|
||||
cabal.project.local
|
||||
|
||||
@ -1,27 +1,36 @@
|
||||
name: blaze-colonnade
|
||||
version: 0.1
|
||||
synopsis: Helper functions for using blaze-html with colonnade
|
||||
description: Blaze HTML and colonnade
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2017 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
name: blaze-colonnade
|
||||
version: 1.2.2.1
|
||||
synopsis: blaze-html backend for colonnade
|
||||
description:
|
||||
This library provides a backend for using blaze-html with colonnade.
|
||||
It generates standard HTML tables with `<table>`, `<tbody>`, `<thead>`,
|
||||
`<tr>`, `<th>`, and `<td>`.
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2017 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
|
||||
-- Note: There is a dependency on profunctors whose only
|
||||
-- purpose is to make doctest work correctly. Since this
|
||||
-- library transitively depends on profunctors anyway,
|
||||
-- this is not a big deal.
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules:
|
||||
Text.Blaze.Colonnade
|
||||
build-depends:
|
||||
base >= 4.7 && < 5
|
||||
, colonnade >= 1.0 && < 1.1
|
||||
base >= 4.8 && < 5
|
||||
, colonnade >= 1.1 && < 1.3
|
||||
, blaze-markup >= 0.7 && < 0.9
|
||||
, blaze-html >= 0.8 && < 0.10
|
||||
, text >= 1.0 && < 1.3
|
||||
, profunctors >= 5.0 && < 5.5
|
||||
, text >= 1.2 && < 1.3
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite test
|
||||
@ -32,6 +41,7 @@ test-suite test
|
||||
base >= 4.7 && <= 5
|
||||
, colonnade
|
||||
, doctest
|
||||
, profunctors
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
-- | Build HTML tables using @blaze-html@ and @colonnade@. The bottom
|
||||
-- of this page has a tutorial that walks through a full example,
|
||||
-- illustrating how to meet typical needs with this library. It is
|
||||
@ -9,9 +13,11 @@
|
||||
-- >>> :module + Colonnade Text.Blaze.Html Text.Blaze.Colonnade
|
||||
-- >>> let col = headed "Grade" (toHtml . fst) <> headed "Letter" (toHtml . snd)
|
||||
-- >>> let rows = [("90-100",'A'),("80-89",'B'),("70-79",'C')]
|
||||
-- >>> printVeryCompactHtml (encodeHeadedHtmlTable mempty col rows)
|
||||
-- >>> printVeryCompactHtml (encodeHtmlTable mempty col rows)
|
||||
-- <table>
|
||||
-- <thead><th>Grade</th><th>Letter</th></thead>
|
||||
-- <thead>
|
||||
-- <tr><th>Grade</th><th>Letter</th></tr>
|
||||
-- </thead>
|
||||
-- <tbody>
|
||||
-- <tr><td>90-100</td><td>A</td></tr>
|
||||
-- <tr><td>80-89</td><td>B</td></tr>
|
||||
@ -20,11 +26,10 @@
|
||||
-- </table>
|
||||
module Text.Blaze.Colonnade
|
||||
( -- * Apply
|
||||
encodeHeadedHtmlTable
|
||||
, encodeHeadlessHtmlTable
|
||||
, encodeHeadedCellTable
|
||||
, encodeHeadlessCellTable
|
||||
encodeHtmlTable
|
||||
, encodeCellTable
|
||||
, encodeTable
|
||||
, encodeCappedTable
|
||||
-- * Cell
|
||||
-- $build
|
||||
, Cell(..)
|
||||
@ -33,11 +38,12 @@ module Text.Blaze.Colonnade
|
||||
, textCell
|
||||
, lazyTextCell
|
||||
, builderCell
|
||||
, htmlFromCell
|
||||
-- * Interactive
|
||||
, printCompactHtml
|
||||
, printVeryCompactHtml
|
||||
-- * Tutorial
|
||||
-- $example
|
||||
-- $setup
|
||||
|
||||
-- * Discussion
|
||||
-- $discussion
|
||||
@ -45,10 +51,11 @@ module Text.Blaze.Colonnade
|
||||
|
||||
import Text.Blaze (Attribute,(!))
|
||||
import Text.Blaze.Html (Html, toHtml)
|
||||
import Colonnade (Colonnade,Headed,Headless)
|
||||
import Colonnade (Colonnade,Headed,Headless,Fascia,Cornice)
|
||||
import Data.Text (Text)
|
||||
import Control.Monad
|
||||
import Data.Monoid
|
||||
import Data.Semigroup
|
||||
import Data.Monoid hiding ((<>))
|
||||
import Data.Foldable
|
||||
import Data.String (IsString(..))
|
||||
import Data.Maybe (listToMaybe)
|
||||
@ -58,22 +65,21 @@ import qualified Text.Blaze.Html.Renderer.Pretty as Pretty
|
||||
import qualified Text.Blaze as Blaze
|
||||
import qualified Text.Blaze.Html5 as H
|
||||
import qualified Text.Blaze.Html5.Attributes as HA
|
||||
import qualified Colonnade.Encode as Encode
|
||||
import qualified Colonnade.Encode as E
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Text.Lazy as LText
|
||||
import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
|
||||
-- $example
|
||||
-- $setup
|
||||
-- We start with a few necessary imports and some example data
|
||||
-- types:
|
||||
--
|
||||
-- >>> :set -XOverloadedStrings
|
||||
-- >>> import Data.Monoid (mconcat,(<>))
|
||||
-- >>> import Data.Char (toLower)
|
||||
-- >>> import Data.Functor.Contravariant (Contravariant(contramap))
|
||||
-- >>> import Colonnade (Colonnade,Headed,Headless,headed)
|
||||
-- >>> import Data.Profunctor (Profunctor(lmap))
|
||||
-- >>> import Colonnade (Colonnade,Headed,Headless,headed,cap,Fascia(..))
|
||||
-- >>> import Text.Blaze.Html (Html, toHtml, toValue)
|
||||
-- >>> import qualified Colonnade as C
|
||||
-- >>> import qualified Text.Blaze.Html5 as H
|
||||
-- >>> data Department = Management | Sales | Engineering deriving (Show,Eq)
|
||||
-- >>> data Employee = Employee { name :: String, department :: Department, age :: Int }
|
||||
@ -93,7 +99,7 @@ import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
-- engineers using a @\<strong\>@ tag.
|
||||
--
|
||||
-- >>> :{
|
||||
-- let tableEmpA :: Colonnade Headed Html Employee
|
||||
-- let tableEmpA :: Colonnade Headed Employee Html
|
||||
-- tableEmpA = mconcat
|
||||
-- [ headed "Name" $ \emp -> case department emp of
|
||||
-- Engineering -> H.strong (toHtml (name emp))
|
||||
@ -110,11 +116,13 @@ import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
-- Let\'s continue:
|
||||
--
|
||||
-- >>> let customAttrs = HA.class_ "stylish-table" <> HA.id "main-table"
|
||||
-- >>> printCompactHtml (encodeHeadedHtmlTable customAttrs tableEmpA employees)
|
||||
-- >>> printCompactHtml (encodeHtmlTable customAttrs tableEmpA employees)
|
||||
-- <table class="stylish-table" id="main-table">
|
||||
-- <thead>
|
||||
-- <th>Name</th>
|
||||
-- <th>Age</th>
|
||||
-- <tr>
|
||||
-- <th>Name</th>
|
||||
-- <th>Age</th>
|
||||
-- </tr>
|
||||
-- </thead>
|
||||
-- <tbody>
|
||||
-- <tr>
|
||||
@ -146,7 +154,7 @@ import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
-- let\'s build a table that encodes departments:
|
||||
--
|
||||
-- >>> :{
|
||||
-- let tableDept :: Colonnade Headed Cell Department
|
||||
-- let tableDept :: Colonnade Headed Department Cell
|
||||
-- tableDept = mconcat
|
||||
-- [ headed "Dept." $ \d -> Cell
|
||||
-- (HA.class_ (toValue (map toLower (show d))))
|
||||
@ -158,48 +166,38 @@ import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
-- literal @"Dept."@ to be accepted as a value of type 'Cell'. To avoid
|
||||
-- this extension, 'stringCell' could be used to upcast the 'String'.
|
||||
-- To try out our 'Colonnade' on a list of departments, we need to use
|
||||
-- 'encodeHeadedCellTable' instead of 'encodeHeadedHtmlTable':
|
||||
-- 'encodeCellTable' instead of 'encodeHtmlTable':
|
||||
--
|
||||
-- >>> let twoDepts = [Sales,Management]
|
||||
-- >>> printCompactHtml (encodeHeadedCellTable customAttrs tableDept twoDepts)
|
||||
-- >>> printVeryCompactHtml (encodeCellTable customAttrs tableDept twoDepts)
|
||||
-- <table class="stylish-table" id="main-table">
|
||||
-- <thead>
|
||||
-- <th>Dept.</th>
|
||||
-- <tr><th>Dept.</th></tr>
|
||||
-- </thead>
|
||||
-- <tbody>
|
||||
-- <tr>
|
||||
-- <td class="sales">Sales</td>
|
||||
-- </tr>
|
||||
-- <tr>
|
||||
-- <td class="management">Management</td>
|
||||
-- </tr>
|
||||
-- <tr><td class="sales">Sales</td></tr>
|
||||
-- <tr><td class="management">Management</td></tr>
|
||||
-- </tbody>
|
||||
-- </table>
|
||||
--
|
||||
-- The attributes on the @\<td\>@ elements show up as they are expected to.
|
||||
-- Now, we take advantage of the @Contravariant@ instance of 'Colonnade' to allow
|
||||
-- Now, we take advantage of the @Profunctor@ instance of 'Colonnade' to allow
|
||||
-- this to work on @Employee@\'s instead:
|
||||
--
|
||||
-- >>> :t contramap
|
||||
-- contramap :: Contravariant f => (a -> b) -> f b -> f a
|
||||
-- >>> let tableEmpB = contramap department tableDept
|
||||
-- >>> :t lmap
|
||||
-- lmap :: Profunctor p => (a -> b) -> p b c -> p a c
|
||||
-- >>> let tableEmpB = lmap department tableDept
|
||||
-- >>> :t tableEmpB
|
||||
-- tableEmpB :: Colonnade Headed Cell Employee
|
||||
-- >>> printCompactHtml (encodeHeadedCellTable customAttrs tableEmpB employees)
|
||||
-- tableEmpB :: Colonnade Headed Employee Cell
|
||||
-- >>> printVeryCompactHtml (encodeCellTable customAttrs tableEmpB employees)
|
||||
-- <table class="stylish-table" id="main-table">
|
||||
-- <thead>
|
||||
-- <th>Dept.</th>
|
||||
-- <tr><th>Dept.</th></tr>
|
||||
-- </thead>
|
||||
-- <tbody>
|
||||
-- <tr>
|
||||
-- <td class="sales">Sales</td>
|
||||
-- </tr>
|
||||
-- <tr>
|
||||
-- <td class="engineering">Engineering</td>
|
||||
-- </tr>
|
||||
-- <tr>
|
||||
-- <td class="management">Management</td>
|
||||
-- </tr>
|
||||
-- <tr><td class="sales">Sales</td></tr>
|
||||
-- <tr><td class="engineering">Engineering</td></tr>
|
||||
-- <tr><td class="management">Management</td></tr>
|
||||
-- </tbody>
|
||||
-- </table>
|
||||
--
|
||||
@ -212,23 +210,25 @@ import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
-- prevents a straightforward monoidal append:
|
||||
--
|
||||
-- >>> :t tableEmpA
|
||||
-- tableEmpA :: Colonnade Headed Html Employee
|
||||
-- tableEmpA :: Colonnade Headed Employee Html
|
||||
-- >>> :t tableEmpB
|
||||
-- tableEmpB :: Colonnade Headed Cell Employee
|
||||
-- tableEmpB :: Colonnade Headed Employee Cell
|
||||
--
|
||||
-- We can upcast the content type with 'Colonnade.mapContent'.
|
||||
-- We can upcast the content type with 'fmap'.
|
||||
-- Monoidal append is then well-typed, and the resulting 'Colonnade'
|
||||
-- can be applied to the employees:
|
||||
--
|
||||
-- >>> let tableEmpC = C.mapContent htmlCell tableEmpA <> tableEmpB
|
||||
-- >>> let tableEmpC = fmap htmlCell tableEmpA <> tableEmpB
|
||||
-- >>> :t tableEmpC
|
||||
-- tableEmpC :: Colonnade Headed Cell Employee
|
||||
-- >>> printCompactHtml (encodeHeadedCellTable customAttrs tableEmpC employees)
|
||||
-- tableEmpC :: Colonnade Headed Employee Cell
|
||||
-- >>> printCompactHtml (encodeCellTable customAttrs tableEmpC employees)
|
||||
-- <table class="stylish-table" id="main-table">
|
||||
-- <thead>
|
||||
-- <th>Name</th>
|
||||
-- <th>Age</th>
|
||||
-- <th>Dept.</th>
|
||||
-- <tr>
|
||||
-- <th>Name</th>
|
||||
-- <th>Age</th>
|
||||
-- <th>Dept.</th>
|
||||
-- </tr>
|
||||
-- </thead>
|
||||
-- <tbody>
|
||||
-- <tr>
|
||||
@ -268,9 +268,12 @@ data Cell = Cell
|
||||
instance IsString Cell where
|
||||
fromString = stringCell
|
||||
|
||||
instance Semigroup Cell where
|
||||
(Cell a1 c1) <> (Cell a2 c2) = Cell (a1 <> a2) (c1 <> c2)
|
||||
|
||||
instance Monoid Cell where
|
||||
mempty = Cell mempty mempty
|
||||
mappend (Cell a1 c1) (Cell a2 c2) = Cell (mappend a1 a2) (mappend c1 c2)
|
||||
mappend = (<>)
|
||||
|
||||
-- | Create a 'Cell' from a 'Widget'
|
||||
htmlCell :: Html -> Cell
|
||||
@ -299,9 +302,8 @@ builderCell = lazyTextCell . TBuilder.toLazyText
|
||||
-- | Encode a table. This handles a very general case and
|
||||
-- is seldom needed by users. One of the arguments provided is
|
||||
-- used to add attributes to the generated @\<tr\>@ elements.
|
||||
encodeTable ::
|
||||
(Foldable f, Foldable h)
|
||||
=> Maybe (Attribute,Attribute) -- ^ Attributes of @\<thead\>@ and its @\<tr\>@, pass 'Nothing' to omit @\<thead\>@
|
||||
encodeTable :: forall h f a c. (Foldable f, E.Headedness h)
|
||||
=> h (Attribute,Attribute) -- ^ Attributes of @\<thead\>@ and its @\<tr\>@, pass 'Nothing' to omit @\<thead\>@
|
||||
-> Attribute -- ^ Attributes of @\<tbody\>@ element
|
||||
-> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element
|
||||
-> ((Html -> Html) -> c -> Html) -- ^ Wrap content and convert to 'Html'
|
||||
@ -311,30 +313,92 @@ encodeTable ::
|
||||
-> Html
|
||||
encodeTable mtheadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
|
||||
H.table ! tableAttrs $ do
|
||||
for_ mtheadAttrs $ \(theadAttrs,theadTrAttrs) -> do
|
||||
H.thead ! theadAttrs $ H.tr ! theadTrAttrs $ do
|
||||
Encode.headerMonoidalGeneral colonnade (wrapContent H.th)
|
||||
case E.headednessExtractForall of
|
||||
Nothing -> return mempty
|
||||
Just extractForall -> do
|
||||
let (theadAttrs,theadTrAttrs) = extract mtheadAttrs
|
||||
H.thead ! theadAttrs $ H.tr ! theadTrAttrs $ do
|
||||
-- E.headerMonoidalGeneral colonnade (wrapContent H.th)
|
||||
foldlMapM' (wrapContent H.th . extract . E.oneColonnadeHead) (E.getColonnade colonnade)
|
||||
where
|
||||
extract :: forall y. h y -> y
|
||||
extract = E.runExtractForall extractForall
|
||||
encodeBody trAttrs wrapContent tbodyAttrs colonnade xs
|
||||
|
||||
encodeTieredHeaderTable :: Foldable f
|
||||
foldlMapM' :: forall g b a m. (Foldable g, Monoid b, Monad m) => (a -> m b) -> g a -> m b
|
||||
foldlMapM' f xs = foldr f' pure xs mempty
|
||||
where
|
||||
f' :: a -> (b -> m b) -> b -> m b
|
||||
f' x k bl = do
|
||||
br <- f x
|
||||
let !b = mappend bl br
|
||||
k b
|
||||
|
||||
-- | Encode a table with tiered header rows.
|
||||
-- >>> let cor = mconcat [cap "Personal" (fmap htmlCell tableEmpA), cap "Work" tableEmpB]
|
||||
-- >>> let fascia = FasciaCap (HA.class_ "category") (FasciaBase (HA.class_ "subcategory"))
|
||||
-- >>> printCompactHtml (encodeCappedCellTable mempty fascia cor [head employees])
|
||||
-- <table>
|
||||
-- <thead>
|
||||
-- <tr class="category">
|
||||
-- <th colspan="2">Personal</th>
|
||||
-- <th colspan="1">Work</th>
|
||||
-- </tr>
|
||||
-- <tr class="subcategory">
|
||||
-- <th colspan="1">Name</th>
|
||||
-- <th colspan="1">Age</th>
|
||||
-- <th colspan="1">Dept.</th>
|
||||
-- </tr>
|
||||
-- </thead>
|
||||
-- <tbody>
|
||||
-- <tr>
|
||||
-- <td>Thaddeus</td>
|
||||
-- <td>34</td>
|
||||
-- <td class="sales">Sales</td>
|
||||
-- </tr>
|
||||
-- </tbody>
|
||||
-- </table>
|
||||
|
||||
encodeCappedCellTable :: Foldable f
|
||||
=> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Fascia p Attribute -- ^ Attributes for @\<tr\>@ elements in the @\<thead\>@
|
||||
-> Cornice Headed p a Cell
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html
|
||||
encodeCappedCellTable = encodeCappedTable mempty mempty (const mempty) htmlFromCell
|
||||
|
||||
-- | Encode a table with tiered header rows. This is the most general function
|
||||
-- in this library for encoding a 'Cornice'.
|
||||
--
|
||||
encodeCappedTable :: Foldable f
|
||||
=> Attribute -- ^ Attributes of @\<thead\>@
|
||||
-> Attribute -- ^ Attributes of @\<tbody\>@ element
|
||||
-> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element in the @\<tbody\>@
|
||||
-> ((Html -> Html) -> c -> Html) -- ^ Wrap content and convert to 'Html'
|
||||
-> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Fascia p Attribute -- ^ Attributes for @\<tr\>@ elements in the @\<thead\>@
|
||||
-> Cornice p a c
|
||||
-> Cornice Headed p a c
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html
|
||||
encodeTieredHeaderTable theadAttrs tbodyAttrs trAttrs wrapContent tableAttrs cornice xs = do
|
||||
let colonnade = CE.discard cornice
|
||||
annCornice = annotate cornice
|
||||
encodeCappedTable theadAttrs tbodyAttrs trAttrs wrapContent tableAttrs fascia cornice xs = do
|
||||
let colonnade = E.discard cornice
|
||||
annCornice = E.annotate cornice
|
||||
H.table ! tableAttrs $ do
|
||||
H.thead ! theadAttrs $ H.tr ! trAttrs $ do
|
||||
Encode.headerMonoidalGeneral colonnade (wrapContent H.th)
|
||||
encodeBody trAttrs wrapContent tbodyAttrs colonnade xs
|
||||
H.thead ! theadAttrs $ do
|
||||
E.headersMonoidal
|
||||
(Just (fascia, \attrs theHtml -> H.tr ! attrs $ theHtml))
|
||||
[ ( \msz c -> case msz of
|
||||
Just sz -> wrapContent H.th c ! HA.colspan (H.toValue (show sz))
|
||||
Nothing -> mempty
|
||||
, id
|
||||
)
|
||||
]
|
||||
annCornice
|
||||
-- H.tr ! trAttrs $ do
|
||||
-- E.headerMonoidalGeneral colonnade (wrapContent H.th)
|
||||
encodeBody trAttrs wrapContent tbodyAttrs colonnade xs
|
||||
|
||||
encodeBody :: (Foldable h, Foldable f)
|
||||
encodeBody :: Foldable f
|
||||
=> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element
|
||||
-> ((Html -> Html) -> c -> Html) -- ^ Wrap content and convert to 'Html'
|
||||
-> Attribute -- ^ Attributes of @\<tbody\>@ element
|
||||
@ -344,53 +408,33 @@ encodeBody :: (Foldable h, Foldable f)
|
||||
encodeBody trAttrs wrapContent tbodyAttrs colonnade xs = do
|
||||
H.tbody ! tbodyAttrs $ do
|
||||
forM_ xs $ \x -> do
|
||||
H.tr ! trAttrs x $ Encode.rowMonoidal colonnade (wrapContent H.td) x
|
||||
H.tr ! trAttrs x $ E.rowMonoidal colonnade (wrapContent H.td) x
|
||||
|
||||
|
||||
-- | Encode a table with a header. Table cells may have attributes
|
||||
-- | Encode a table. Table cells may have attributes
|
||||
-- applied to them.
|
||||
encodeHeadedCellTable ::
|
||||
encodeCellTable ::
|
||||
Foldable f
|
||||
=> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade Headed a Cell -- ^ How to encode data as columns
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html
|
||||
encodeHeadedCellTable = encodeTable
|
||||
(Just (mempty,mempty)) mempty (const mempty) htmlFromCell
|
||||
encodeCellTable = encodeTable
|
||||
(E.headednessPure (mempty,mempty)) mempty (const mempty) htmlFromCell
|
||||
|
||||
-- | Encode a table without a header. Table cells may have attributes
|
||||
-- applied to them.
|
||||
encodeHeadlessCellTable ::
|
||||
Foldable f
|
||||
-- | Encode a table. Table cell element do not have
|
||||
-- any attributes applied to them.
|
||||
encodeHtmlTable ::
|
||||
(Foldable f, E.Headedness h)
|
||||
=> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade Headless a Cell -- ^ How to encode data as columns
|
||||
-> Colonnade h a Html -- ^ How to encode data as columns
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html
|
||||
encodeHeadlessCellTable = encodeTable
|
||||
Nothing mempty (const mempty) htmlFromCell
|
||||
|
||||
-- | Encode a table with a header. Table cells cannot have attributes
|
||||
-- applied to them.
|
||||
encodeHeadedHtmlTable ::
|
||||
Foldable f
|
||||
=> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade Headed a Html -- ^ How to encode data as columns
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html
|
||||
encodeHeadedHtmlTable = encodeTable
|
||||
(Just (mempty,mempty)) mempty (const mempty) ($)
|
||||
|
||||
-- | Encode a table without a header. Table cells cannot have attributes
|
||||
-- applied to them.
|
||||
encodeHeadlessHtmlTable ::
|
||||
Foldable f
|
||||
=> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade Headless a Html -- ^ How to encode data as columns
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html
|
||||
encodeHeadlessHtmlTable = encodeTable
|
||||
Nothing mempty (const mempty) ($)
|
||||
encodeHtmlTable = encodeTable
|
||||
(E.headednessPure (mempty,mempty)) mempty (const mempty) ($)
|
||||
|
||||
-- | Convert a 'Cell' to 'Html' by wrapping the content with a tag
|
||||
-- and applying the 'Cell' attributes to that tag.
|
||||
htmlFromCell :: (Html -> Html) -> Cell -> Html
|
||||
htmlFromCell f (Cell attr content) = f ! attr $ content
|
||||
|
||||
@ -477,7 +521,6 @@ printVeryCompactHtml = putStrLn
|
||||
. removeWhitespaceAfterTag "span"
|
||||
. removeWhitespaceAfterTag "em"
|
||||
. removeWhitespaceAfterTag "tr"
|
||||
. removeWhitespaceAfterTag "thead"
|
||||
. Pretty.renderHtml
|
||||
|
||||
|
||||
|
||||
16
build
Executable file
16
build
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# To use this script on Ubuntu, you will need to first run the following:
|
||||
#
|
||||
# sudo apt install ghc-7.4.2 ghc-7.6.3 ghc-7.8.4 ghc-7.10.3 ghc-8.0.2 ghc-8.2.2 ghc-8.4.3 ghc-8.6.1
|
||||
|
||||
declare -a ghcs=("7.10.3" "8.0.2" "8.2.2" "8.4.4" "8.6.5")
|
||||
|
||||
## now loop through the above array
|
||||
for g in "${ghcs[@]}"
|
||||
do
|
||||
cabal new-build --builddir "dist-$g" -w "/opt/ghc/bin/ghc-$g" colonnade
|
||||
cabal new-build --builddir "dist-$g" -w "/opt/ghc/bin/ghc-$g" siphon
|
||||
done
|
||||
|
||||
4
cabal.project
Normal file
4
cabal.project
Normal file
@ -0,0 +1,4 @@
|
||||
packages: ./colonnade
|
||||
./blaze-colonnade
|
||||
./lucid-colonnade
|
||||
./siphon
|
||||
@ -1,8 +1,8 @@
|
||||
name: colonnade
|
||||
version: 1.0.0
|
||||
synopsis: Generic types and functions for columnar encoding and decoding
|
||||
name: colonnade
|
||||
version: 1.2.0.2
|
||||
synopsis: Generic types and functions for columnar encoding and decoding
|
||||
description:
|
||||
The `colonnade` package provides a way to to talk about
|
||||
The `colonnade` package provides a way to talk about
|
||||
columnar encodings and decodings of data. This package provides
|
||||
very general types and does not provide a way for the end-user
|
||||
to actually apply the columnar encodings they build to data.
|
||||
@ -10,46 +10,53 @@ description:
|
||||
that provides (1) a content type and (2) functions for feeding
|
||||
data into a columnar encoding:
|
||||
.
|
||||
* <https://hackage.haskell.org/package/lucid-colonnade lucid-colonnade> for `lucid` html tables
|
||||
.
|
||||
* <https://hackage.haskell.org/package/blaze-colonnade blaze-colonnade> for `blaze` html tables
|
||||
.
|
||||
* <https://hackage.haskell.org/package/reflex-dom-colonnade reflex-dom-colonnade> for reactive `reflex-dom` tables
|
||||
.
|
||||
* <https://hackage.haskell.org/package/yesod-colonnade yesod-colonnade> for `yesod` widgets
|
||||
.
|
||||
* <http://hackage.haskell.org/package/siphon siphon> for encoding and decoding CSVs
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2016 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2016 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules:
|
||||
Colonnade
|
||||
Colonnade.Encode
|
||||
Colonnade.Internal
|
||||
Colonnade.Cornice.Encode
|
||||
build-depends:
|
||||
base >= 4.7 && < 5
|
||||
, contravariant >= 1.2 && < 1.5
|
||||
base >= 4.8 && < 5
|
||||
, contravariant >= 1.2 && < 1.6
|
||||
, vector >= 0.10 && < 0.13
|
||||
, text >= 1.0 && < 1.3
|
||||
, bytestring >= 0.10 && < 0.11
|
||||
, profunctors >= 4.0 && < 5.3
|
||||
, profunctors >= 5.0 && < 5.5
|
||||
, semigroups >= 0.18.2 && < 0.20
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
||||
|
||||
test-suite test
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Main.hs
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Main.hs
|
||||
build-depends:
|
||||
base >= 4.7 && <= 5
|
||||
, colonnade
|
||||
, doctest
|
||||
, semigroupoids
|
||||
, ansi-wl-pprint
|
||||
, QuickCheck
|
||||
, fast-logger
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
|
||||
8
colonnade/default.nix
Normal file
8
colonnade/default.nix
Normal file
@ -0,0 +1,8 @@
|
||||
{ frontend ? false }:
|
||||
let
|
||||
pname = "colonnade";
|
||||
main = (import ../nix/default.nix {
|
||||
inherit frontend;
|
||||
});
|
||||
in
|
||||
main.${pname}
|
||||
@ -1,63 +0,0 @@
|
||||
import Colonnade.Encoding
|
||||
import Colonnade.Types
|
||||
import Data.Functor.Contravariant
|
||||
|
||||
data Color = Red | Green | Blue deriving (Show)
|
||||
data Person = Person { personName :: String, personAge :: Int }
|
||||
data House = House { houseColor :: Color, housePrice :: Int }
|
||||
|
||||
encodingPerson :: Encoding Headed String Person
|
||||
encodingPerson = mconcat
|
||||
[ headed "Name" personName
|
||||
, headed "Age" (show . personAge)
|
||||
]
|
||||
|
||||
encodingHouse :: Encoding Headed String House
|
||||
encodingHouse = mconcat
|
||||
[ headed "Color" (show . houseColor)
|
||||
, headed "Price" (('$':) . show . housePrice)
|
||||
]
|
||||
|
||||
encodingPerson2 :: Encoding Headless String Person
|
||||
encodingPerson2 = mconcat
|
||||
[ headless personName
|
||||
, headless (show . personAge)
|
||||
]
|
||||
|
||||
people :: [Person]
|
||||
people = [Person "David" 63, Person "Ava" 34, Person "Sonia" 12]
|
||||
|
||||
houses :: [House]
|
||||
houses = [House Green 170000, House Blue 115000]
|
||||
|
||||
peopleInHouses :: [(Person,House)]
|
||||
peopleInHouses = (,) <$> people <*> houses
|
||||
|
||||
encodingPersonHouse :: Encoding Headed String (Person,House)
|
||||
encodingPersonHouse = mconcat
|
||||
[ contramap fst encodingPerson
|
||||
, contramap snd encodingHouse
|
||||
]
|
||||
|
||||
owners :: [(Person,Maybe House)]
|
||||
owners =
|
||||
[ (Person "Jordan" 18, Nothing)
|
||||
, (Person "Ruth" 25, Just (House Red 125000))
|
||||
, (Person "Sonia" 12, Just (House Green 145000))
|
||||
]
|
||||
|
||||
encodingOwners :: Encoding Headed String (Person,Maybe House)
|
||||
encodingOwners = mconcat
|
||||
[ contramap fst encodingPerson
|
||||
, contramap snd (fromMaybe "(none)" encodingHouse)
|
||||
]
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStr $ ascii encodingPerson people
|
||||
putStrLn ""
|
||||
putStr $ ascii encodingHouse houses
|
||||
putStrLn ""
|
||||
putStr $ ascii encodingOwners owners
|
||||
putStrLn ""
|
||||
|
||||
1
colonnade/shell.nix
Normal file
1
colonnade/shell.nix
Normal file
@ -0,0 +1 @@
|
||||
(import ./. {}).env
|
||||
@ -1,4 +1,7 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
-- | Build backend-agnostic columnar encodings that can be
|
||||
-- used to visualize tabular data.
|
||||
@ -7,19 +10,25 @@ module Colonnade
|
||||
-- $setup
|
||||
-- * Types
|
||||
Colonnade
|
||||
, Headed
|
||||
, Headless
|
||||
, Headed(..)
|
||||
, Headless(..)
|
||||
-- * Typeclasses
|
||||
, E.Headedness(..)
|
||||
-- * Create
|
||||
, headed
|
||||
, headless
|
||||
, singleton
|
||||
-- * Transform
|
||||
, mapHeaderContent
|
||||
-- ** Body
|
||||
, fromMaybe
|
||||
, columns
|
||||
, bool
|
||||
, replaceWhen
|
||||
, modifyWhen
|
||||
-- ** Header
|
||||
, mapHeaderContent
|
||||
, mapHeadedness
|
||||
, toHeadless
|
||||
-- * Cornice
|
||||
-- ** Types
|
||||
, Cornice
|
||||
@ -30,16 +39,16 @@ module Colonnade
|
||||
, recap
|
||||
-- * Ascii Table
|
||||
, ascii
|
||||
, asciiCapped
|
||||
) where
|
||||
|
||||
import Colonnade.Internal
|
||||
import Colonnade.Encode (Colonnade,Cornice,
|
||||
Pillar(..),Fascia(..),Headed(..),Headless(..))
|
||||
import Data.Foldable
|
||||
import Data.Monoid (Endo(..))
|
||||
import Control.Monad
|
||||
import qualified Colonnade.Encode as Encode
|
||||
import qualified Colonnade.Cornice.Encode as CE
|
||||
import qualified Data.Bool
|
||||
import qualified Data.Maybe
|
||||
import qualified Colonnade.Encode as E
|
||||
import qualified Data.List as List
|
||||
import qualified Data.Vector as Vector
|
||||
|
||||
@ -108,13 +117,23 @@ headless = singleton Headless
|
||||
|
||||
-- | A single column with any kind of header. This is not typically needed.
|
||||
singleton :: h c -> (a -> c) -> Colonnade h a c
|
||||
singleton h = Colonnade . Vector.singleton . OneColonnade h
|
||||
singleton h = E.Colonnade . Vector.singleton . E.OneColonnade h
|
||||
|
||||
-- | Map over the content in the header. This is similar performing 'fmap'
|
||||
-- on a 'Colonnade' except that the body content is unaffected.
|
||||
mapHeaderContent :: Functor h => (c -> c) -> Colonnade h a c -> Colonnade h a c
|
||||
mapHeaderContent f (Colonnade v) =
|
||||
Colonnade (Vector.map (\(OneColonnade h e) -> OneColonnade (fmap f h) e) v)
|
||||
mapHeaderContent f (E.Colonnade v) =
|
||||
E.Colonnade (Vector.map (\(E.OneColonnade h e) -> E.OneColonnade (fmap f h) e) v)
|
||||
|
||||
-- | Map over the header type of a 'Colonnade'.
|
||||
mapHeadedness :: (forall x. h x -> h' x) -> Colonnade h a c -> Colonnade h' a c
|
||||
mapHeadedness f (E.Colonnade v) =
|
||||
E.Colonnade (Vector.map (\(E.OneColonnade h e) -> E.OneColonnade (f h) e) v)
|
||||
|
||||
-- | Remove the heading from a 'Colonnade'.
|
||||
toHeadless :: Colonnade h a c -> Colonnade Headless a c
|
||||
toHeadless = mapHeadedness (const Headless)
|
||||
|
||||
|
||||
-- | Lift a column over a 'Maybe'. For example, if some people
|
||||
-- have houses and some do not, the data that pairs them together
|
||||
@ -149,8 +168,8 @@ mapHeaderContent f (Colonnade v) =
|
||||
-- | Sonia | 12 | Green | $145000 |
|
||||
-- +--------+-----+-------+---------+
|
||||
fromMaybe :: c -> Colonnade f a c -> Colonnade f (Maybe a) c
|
||||
fromMaybe c (Colonnade v) = Colonnade $ flip Vector.map v $
|
||||
\(OneColonnade h encode) -> OneColonnade h (maybe c encode)
|
||||
fromMaybe c (E.Colonnade v) = E.Colonnade $ flip Vector.map v $
|
||||
\(E.OneColonnade h encode) -> E.OneColonnade h (maybe c encode)
|
||||
|
||||
-- | Convert a collection of @b@ values into a columnar encoding of
|
||||
-- the same size. Suppose we decide to show a house\'s color
|
||||
@ -178,8 +197,8 @@ columns :: Foldable g
|
||||
-> g b -- ^ Basis for column encodings
|
||||
-> Colonnade f a c
|
||||
columns getCell getHeader = id
|
||||
. Colonnade
|
||||
. Vector.map (\b -> OneColonnade (getHeader b) (getCell b))
|
||||
. E.Colonnade
|
||||
. Vector.map (\b -> E.OneColonnade (getHeader b) (getCell b))
|
||||
. Vector.fromList
|
||||
. toList
|
||||
|
||||
@ -200,9 +219,9 @@ modifyWhen ::
|
||||
-> (a -> Bool) -- ^ Row predicate
|
||||
-> Colonnade f a c -- ^ Original 'Colonnade'
|
||||
-> Colonnade f a c
|
||||
modifyWhen changeContent p (Colonnade v) = Colonnade
|
||||
modifyWhen changeContent p (E.Colonnade v) = E.Colonnade
|
||||
( Vector.map
|
||||
(\(OneColonnade h encode) -> OneColonnade h $ \a ->
|
||||
(\(E.OneColonnade h encode) -> E.OneColonnade h $ \a ->
|
||||
if p a then changeContent (encode a) else encode a
|
||||
) v
|
||||
)
|
||||
@ -214,12 +233,7 @@ replaceWhen ::
|
||||
-> (a -> Bool) -- ^ Row predicate
|
||||
-> Colonnade f a c -- ^ Original 'Colonnade'
|
||||
-> Colonnade f a c
|
||||
replaceWhen newContent p (Colonnade v) = Colonnade
|
||||
( Vector.map
|
||||
(\(OneColonnade h encode) -> OneColonnade h $ \a ->
|
||||
if p a then newContent else encode a
|
||||
) v
|
||||
)
|
||||
replaceWhen = modifyWhen . const
|
||||
|
||||
-- | Augment a 'Colonnade' with a header spans over all of the
|
||||
-- existing headers. This is best demonstrated by example.
|
||||
@ -260,7 +274,7 @@ replaceWhen newContent p (Colonnade v) = Colonnade
|
||||
--
|
||||
-- >>> let cor = mconcat [cap "Person" colPersonFst, cap "House" colHouseSnd]
|
||||
-- >>> :t cor
|
||||
-- cor :: Cornice ('Cap 'Base) (Person, House) [Char]
|
||||
-- cor :: Cornice Headed ('Cap 'Base) (Person, House) [Char]
|
||||
-- >>> putStr (asciiCapped cor personHomePairs)
|
||||
-- +-------------+-----------------+
|
||||
-- | Person | House |
|
||||
@ -272,8 +286,8 @@ replaceWhen newContent p (Colonnade v) = Colonnade
|
||||
-- | Sonia | 12 | Green | $150000 |
|
||||
-- +-------+-----+-------+---------+
|
||||
--
|
||||
cap :: c -> Colonnade Headed a c -> Cornice (Cap Base) a c
|
||||
cap h = CorniceCap . Vector.singleton . OneCornice h . CorniceBase
|
||||
cap :: c -> Colonnade h a c -> Cornice h (Cap Base) a c
|
||||
cap h = E.CorniceCap . Vector.singleton . E.OneCornice h . E.CorniceBase
|
||||
|
||||
-- | Add another cap to a cornice. There is no limit to how many times
|
||||
-- this can be applied:
|
||||
@ -307,21 +321,29 @@ cap h = CorniceCap . Vector.singleton . OneCornice h . CorniceBase
|
||||
-- | Weekday | $8 | $8 | $8 | $6 | $7 | $8 | $8 | $8 | $6 | $7 |
|
||||
-- | Weekend | $9 | $9 | $9 | $7 | $8 | $9 | $9 | $9 | $7 | $8 |
|
||||
-- +---------+----+----+----+------+-------+----+----+----+------+-------+
|
||||
recap :: c -> Cornice p a c -> Cornice (Cap p) a c
|
||||
recap h cor = CorniceCap (Vector.singleton (OneCornice h cor))
|
||||
recap :: c -> Cornice h p a c -> Cornice h (Cap p) a c
|
||||
recap h cor = E.CorniceCap (Vector.singleton (E.OneCornice h cor))
|
||||
|
||||
asciiCapped :: Foldable f
|
||||
=> Cornice p a String -- ^ columnar encoding
|
||||
=> Cornice Headed p a String -- ^ columnar encoding
|
||||
-> f a -- ^ rows
|
||||
-> String
|
||||
asciiCapped cor xs =
|
||||
let annCor = CE.annotateFinely (\x y -> x + y + 3) id
|
||||
let annCor = E.annotateFinely (\x y -> x + y + 3) id
|
||||
List.length xs cor
|
||||
sizedCol = CE.uncapAnnotated annCor
|
||||
in CE.headersMonoidal
|
||||
sizedCol = E.uncapAnnotated annCor
|
||||
in E.headersMonoidal
|
||||
Nothing
|
||||
[ (\sz c -> hyphens (sz + 2) ++ "+", \s -> "+" ++ s ++ "\n")
|
||||
, (\sz c -> " " ++ rightPad sz ' ' c ++ " |", \s -> "|" ++ s ++ "\n")
|
||||
[ ( \msz _ -> case msz of
|
||||
Just sz -> "+" ++ hyphens (sz + 2)
|
||||
Nothing -> ""
|
||||
, \s -> s ++ "+\n"
|
||||
)
|
||||
, ( \msz c -> case msz of
|
||||
Just sz -> "| " ++ rightPad sz ' ' c ++ " "
|
||||
Nothing -> ""
|
||||
, \s -> s ++ "|\n"
|
||||
)
|
||||
] annCor ++ asciiBody sizedCol xs
|
||||
|
||||
|
||||
@ -335,43 +357,51 @@ ascii :: Foldable f
|
||||
-> f a -- ^ rows
|
||||
-> String
|
||||
ascii col xs =
|
||||
let sizedCol = Encode.sizeColumns List.length xs col
|
||||
let sizedCol = E.sizeColumns List.length xs col
|
||||
divider = concat
|
||||
[ "+"
|
||||
, Encode.headerMonoidalFull sizedCol
|
||||
(\(Sized sz _) -> hyphens (sz + 2) ++ "+")
|
||||
, "\n"
|
||||
[ E.headerMonoidalFull sizedCol
|
||||
(\(E.Sized msz _) -> case msz of
|
||||
Just sz -> "+" ++ hyphens (sz + 2)
|
||||
Nothing -> ""
|
||||
)
|
||||
, "+\n"
|
||||
]
|
||||
in List.concat
|
||||
[ divider
|
||||
, concat
|
||||
[ "|"
|
||||
, Encode.headerMonoidalFull sizedCol
|
||||
(\(Sized s (Headed h)) -> " " ++ rightPad s ' ' h ++ " |")
|
||||
, "\n"
|
||||
[ E.headerMonoidalFull sizedCol
|
||||
(\(E.Sized msz (Headed h)) -> case msz of
|
||||
Just sz -> "| " ++ rightPad sz ' ' h ++ " "
|
||||
Nothing -> ""
|
||||
)
|
||||
, "|\n"
|
||||
]
|
||||
, asciiBody sizedCol xs
|
||||
]
|
||||
|
||||
asciiBody :: Foldable f
|
||||
=> Colonnade (Sized Headed) a String
|
||||
=> Colonnade (E.Sized (Maybe Int) Headed) a String
|
||||
-> f a
|
||||
-> String
|
||||
asciiBody sizedCol xs =
|
||||
let divider = concat
|
||||
[ "+"
|
||||
, Encode.headerMonoidalFull sizedCol
|
||||
(\(Sized sz _) -> hyphens (sz + 2) ++ "+")
|
||||
, "\n"
|
||||
[ E.headerMonoidalFull sizedCol
|
||||
(\(E.Sized msz _) -> case msz of
|
||||
Just sz -> "+" ++ hyphens (sz + 2)
|
||||
Nothing -> ""
|
||||
)
|
||||
, "+\n"
|
||||
]
|
||||
rowContents = foldMap
|
||||
(\x -> concat
|
||||
[ "|"
|
||||
, Encode.rowMonoidalHeader
|
||||
[ E.rowMonoidalHeader
|
||||
sizedCol
|
||||
(\(Sized sz _) c -> " " ++ rightPad sz ' ' c ++ " |")
|
||||
(\(E.Sized msz _) c -> case msz of
|
||||
Nothing -> ""
|
||||
Just sz -> "| " ++ rightPad sz ' ' c ++ " "
|
||||
)
|
||||
x
|
||||
, "\n"
|
||||
, "|\n"
|
||||
]
|
||||
) xs
|
||||
in List.concat
|
||||
|
||||
@ -1,213 +0,0 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-unticked-promoted-constructors -Werror #-}
|
||||
module Colonnade.Cornice.Encode
|
||||
( annotate
|
||||
, annotateFinely
|
||||
, size
|
||||
, endow
|
||||
, discard
|
||||
, headersMonoidal
|
||||
, uncapAnnotated
|
||||
) where
|
||||
|
||||
import Colonnade.Internal
|
||||
import Data.Vector (Vector)
|
||||
import Control.Monad.ST (ST,runST)
|
||||
import Data.Monoid
|
||||
import qualified Data.Vector as V
|
||||
import qualified Colonnade.Encode as E
|
||||
|
||||
discard :: Cornice p a c -> Colonnade Headed a c
|
||||
discard = go where
|
||||
go :: forall p a c. Cornice p a c -> Colonnade Headed a c
|
||||
go (CorniceBase c) = c
|
||||
go (CorniceCap children) = Colonnade (getColonnade . go . oneCorniceBody =<< children)
|
||||
|
||||
endow :: forall p a c. (c -> c -> c) -> Cornice p a c -> Colonnade Headed a c
|
||||
endow f x = case x of
|
||||
CorniceBase colonnade -> colonnade
|
||||
CorniceCap v -> Colonnade (V.concatMap (\(OneCornice h b) -> go h b) v)
|
||||
where
|
||||
go :: forall p'. c -> Cornice p' a c -> Vector (OneColonnade Headed a c)
|
||||
go c (CorniceBase (Colonnade v)) = V.map (mapOneColonnadeHeader (f c)) v
|
||||
go c (CorniceCap v) = V.concatMap (\(OneCornice h b) -> go (f c h) b) v
|
||||
|
||||
uncapAnnotated :: forall p a c. AnnotatedCornice p a c -> Colonnade (Sized Headed) a c
|
||||
uncapAnnotated x = case x of
|
||||
AnnotatedCorniceBase _ colonnade -> colonnade
|
||||
AnnotatedCorniceCap _ v -> Colonnade (V.concatMap (\(OneCornice _ b) -> go b) v)
|
||||
where
|
||||
go :: forall p'. AnnotatedCornice p' a c -> Vector (OneColonnade (Sized Headed) a c)
|
||||
go (AnnotatedCorniceBase _ (Colonnade v)) = v
|
||||
go (AnnotatedCorniceCap _ v) = V.concatMap (\(OneCornice _ b) -> go b) v
|
||||
|
||||
annotate :: Cornice p a c -> AnnotatedCornice p a c
|
||||
annotate = go where
|
||||
go :: forall p a c. Cornice p a c -> AnnotatedCornice p a c
|
||||
go (CorniceBase c) = let len = V.length (getColonnade c) in
|
||||
AnnotatedCorniceBase
|
||||
(if len > 0 then (Just len) else Nothing)
|
||||
(mapHeadedness (Sized 1) c)
|
||||
go (CorniceCap children) =
|
||||
let annChildren = fmap (mapOneCorniceBody go) children
|
||||
in AnnotatedCorniceCap
|
||||
( ( ( V.foldl' (combineJustInt (+))
|
||||
) Nothing . V.map (size . oneCorniceBody)
|
||||
) annChildren
|
||||
)
|
||||
annChildren
|
||||
|
||||
combineJustInt :: (Int -> Int -> Int) -> Maybe Int -> Maybe Int -> Maybe Int
|
||||
combineJustInt f acc el = case acc of
|
||||
Nothing -> case el of
|
||||
Nothing -> Nothing
|
||||
Just i -> Just i
|
||||
Just i -> case el of
|
||||
Nothing -> Just i
|
||||
Just j -> Just (f i j)
|
||||
|
||||
mapJustInt :: (Int -> Int) -> Maybe Int -> Maybe Int
|
||||
mapJustInt _ Nothing = Nothing
|
||||
mapJustInt f (Just i) = Just (f i)
|
||||
|
||||
annotateFinely :: Foldable f
|
||||
=> (Int -> Int -> Int) -- ^ fold function
|
||||
-> (Int -> Int) -- ^ finalize
|
||||
-> (c -> Int) -- ^ Get size from content
|
||||
-> f a
|
||||
-> Cornice p a c
|
||||
-> AnnotatedCornice p a c
|
||||
annotateFinely g finish toSize xs cornice = runST $ do
|
||||
m <- newMutableSizedCornice cornice
|
||||
sizeColonnades toSize xs m
|
||||
freezeMutableSizedCornice g finish m
|
||||
|
||||
sizeColonnades :: forall f s p a c.
|
||||
Foldable f
|
||||
=> (c -> Int) -- ^ Get size from content
|
||||
-> f a
|
||||
-> MutableSizedCornice s p a c
|
||||
-> ST s ()
|
||||
sizeColonnades toSize xs cornice = do
|
||||
goHeader cornice
|
||||
mapM_ (goRow cornice) xs
|
||||
where
|
||||
goRow :: forall p'. MutableSizedCornice s p' a c -> a -> ST s ()
|
||||
goRow (MutableSizedCorniceBase c) a = E.rowUpdateSize toSize c a
|
||||
goRow (MutableSizedCorniceCap children) a = mapM_ (flip goRow a . oneCorniceBody) children
|
||||
goHeader :: forall p'. MutableSizedCornice s p' a c -> ST s ()
|
||||
goHeader (MutableSizedCorniceBase c) = E.headerUpdateSize toSize c
|
||||
goHeader (MutableSizedCorniceCap children) = mapM_ (goHeader . oneCorniceBody) children
|
||||
|
||||
freezeMutableSizedCornice :: forall s p a c.
|
||||
(Int -> Int -> Int) -- ^ fold function
|
||||
-> (Int -> Int) -- ^ finalize
|
||||
-> MutableSizedCornice s p a c
|
||||
-> ST s (AnnotatedCornice p a c)
|
||||
freezeMutableSizedCornice step finish = go
|
||||
where
|
||||
go :: forall p' a' c'. MutableSizedCornice s p' a' c' -> ST s (AnnotatedCornice p' a' c')
|
||||
go (MutableSizedCorniceBase msc) = do
|
||||
szCol <- E.freezeMutableSizedColonnade msc
|
||||
let sz =
|
||||
( mapJustInt finish
|
||||
. V.foldl' (combineJustInt step) Nothing
|
||||
. V.map (Just . sizedSize . oneColonnadeHead)
|
||||
) (getColonnade szCol)
|
||||
return (AnnotatedCorniceBase sz szCol)
|
||||
go (MutableSizedCorniceCap v1) = do
|
||||
v2 <- V.mapM (traverseOneCorniceBody go) v1
|
||||
let sz =
|
||||
( mapJustInt finish
|
||||
. V.foldl' (combineJustInt step) Nothing
|
||||
. V.map (size . oneCorniceBody)
|
||||
) v2
|
||||
return $ AnnotatedCorniceCap sz v2
|
||||
|
||||
newMutableSizedCornice :: forall s p a c.
|
||||
Cornice p a c
|
||||
-> ST s (MutableSizedCornice s p a c)
|
||||
newMutableSizedCornice = go where
|
||||
go :: forall p'. Cornice p' a c -> ST s (MutableSizedCornice s p' a c)
|
||||
go (CorniceBase c) = fmap MutableSizedCorniceBase (E.newMutableSizedColonnade c)
|
||||
go (CorniceCap v) = fmap MutableSizedCorniceCap (V.mapM (traverseOneCorniceBody go) v)
|
||||
|
||||
traverseOneCorniceBody :: Monad m => (k p a c -> m (j p a c)) -> OneCornice k p a c -> m (OneCornice j p a c)
|
||||
traverseOneCorniceBody f (OneCornice h b) = fmap (OneCornice h) (f b)
|
||||
|
||||
mapHeadedness :: (forall x. h x -> h' x) -> Colonnade h a c -> Colonnade h' a c
|
||||
mapHeadedness f (Colonnade v) =
|
||||
Colonnade (V.map (\(OneColonnade h c) -> OneColonnade (f h) c) v)
|
||||
|
||||
|
||||
-- | This is an O(1) operation, sort of
|
||||
size :: AnnotatedCornice p a c -> Maybe Int
|
||||
size x = case x of
|
||||
AnnotatedCorniceBase m _ -> m
|
||||
AnnotatedCorniceCap sz _ -> sz
|
||||
|
||||
mapOneCorniceBody :: (forall p' a' c'. k p' a' c' -> j p' a' c') -> OneCornice k p a c -> OneCornice j p a c
|
||||
mapOneCorniceBody f (OneCornice h b) = OneCornice h (f b)
|
||||
|
||||
mapOneColonnadeHeader :: Functor h => (c -> c) -> OneColonnade h a c -> OneColonnade h a c
|
||||
mapOneColonnadeHeader f (OneColonnade h b) = OneColonnade (fmap f h) b
|
||||
|
||||
headersMonoidal :: forall r m c p a.
|
||||
Monoid m
|
||||
=> Maybe (Fascia p r, r -> m -> m) -- ^ Apply the Fascia header row content
|
||||
-> [(Int -> c -> m, m -> m)] -- ^ Build content from cell content and size
|
||||
-> AnnotatedCornice p a c
|
||||
-> m
|
||||
headersMonoidal wrapRow fromContentList = go wrapRow
|
||||
where
|
||||
go :: forall p'. Maybe (Fascia p' r, r -> m -> m) -> AnnotatedCornice p' a c -> m
|
||||
go ef (AnnotatedCorniceBase _ (Colonnade v)) =
|
||||
let g :: m -> m
|
||||
g m = case ef of
|
||||
Nothing -> m
|
||||
Just (FasciaBase r, f) -> f r m
|
||||
in g $ foldMap (\(fromContent,wrap) -> wrap
|
||||
(foldMap (\(OneColonnade (Sized sz (Headed h)) _) ->
|
||||
(fromContent sz h)) v)) fromContentList
|
||||
go ef (AnnotatedCorniceCap _ v) =
|
||||
let g :: m -> m
|
||||
g m = case ef of
|
||||
Nothing -> m
|
||||
Just (FasciaCap r _, f) -> f r m
|
||||
in g (foldMap (\(fromContent,wrap) -> wrap (foldMap (\(OneCornice h b) ->
|
||||
(case size b of
|
||||
Nothing -> mempty
|
||||
Just sz -> fromContent sz h)
|
||||
) v)) fromContentList)
|
||||
<> case ef of
|
||||
Nothing -> case flattenAnnotated v of
|
||||
Nothing -> mempty
|
||||
Just annCoreNext -> go Nothing annCoreNext
|
||||
Just (FasciaCap _ fn, f) -> case flattenAnnotated v of
|
||||
Nothing -> mempty
|
||||
Just annCoreNext -> go (Just (fn,f)) annCoreNext
|
||||
|
||||
flattenAnnotated :: Vector (OneCornice AnnotatedCornice p a c) -> Maybe (AnnotatedCornice p a c)
|
||||
flattenAnnotated v = case v V.!? 0 of
|
||||
Nothing -> Nothing
|
||||
Just (OneCornice _ x) -> Just $ case x of
|
||||
AnnotatedCorniceBase m _ -> flattenAnnotatedBase m v
|
||||
AnnotatedCorniceCap m _ -> flattenAnnotatedCap m v
|
||||
|
||||
flattenAnnotatedBase :: Maybe Int -> Vector (OneCornice AnnotatedCornice Base a c) -> AnnotatedCornice Base a c
|
||||
flattenAnnotatedBase msz = AnnotatedCorniceBase msz
|
||||
. Colonnade
|
||||
. V.concatMap
|
||||
(\(OneCornice _ (AnnotatedCorniceBase _ (Colonnade v))) -> v)
|
||||
|
||||
flattenAnnotatedCap :: Maybe Int -> Vector (OneCornice AnnotatedCornice (Cap p) a c) -> AnnotatedCornice (Cap p) a c
|
||||
flattenAnnotatedCap m = AnnotatedCorniceCap m . V.concatMap getTheVector
|
||||
|
||||
getTheVector :: OneCornice AnnotatedCornice (Cap p) a c -> Vector (OneCornice AnnotatedCornice p a c)
|
||||
getTheVector (OneCornice _ (AnnotatedCorniceCap _ v)) = v
|
||||
|
||||
|
||||
@ -1,174 +0,0 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
module Colonnade.Decoding where
|
||||
|
||||
import Colonnade.Types
|
||||
import Data.Functor.Contravariant
|
||||
import Data.Vector (Vector)
|
||||
import qualified Data.Vector as Vector
|
||||
import Data.Char (chr)
|
||||
|
||||
-- | Converts the content type of a 'Decolonnade'. The @'Contravariant' f@
|
||||
-- constraint means that @f@ can be 'Headless' but not 'Headed'.
|
||||
contramapContent :: forall c1 c2 f a. Contravariant f => (c2 -> c1) -> Decolonnade f c1 a -> Decolonnade f c2 a
|
||||
contramapContent f = go
|
||||
where
|
||||
go :: forall b. Decolonnade f c1 b -> Decolonnade f c2 b
|
||||
go (DecolonnadePure x) = DecolonnadePure x
|
||||
go (DecolonnadeAp h decode apNext) =
|
||||
DecolonnadeAp (contramap f h) (decode . f) (go apNext)
|
||||
|
||||
headless :: (content -> Either String a) -> Decolonnade Headless content a
|
||||
headless f = DecolonnadeAp Headless f (DecolonnadePure id)
|
||||
|
||||
headed :: content -> (content -> Either String a) -> Decolonnade Headed content a
|
||||
headed h f = DecolonnadeAp (Headed h) f (DecolonnadePure id)
|
||||
|
||||
indexed :: Int -> (content -> Either String a) -> Decolonnade (Indexed Headless) content a
|
||||
indexed ix f = DecolonnadeAp (Indexed ix Headless) f (DecolonnadePure id)
|
||||
|
||||
maxIndex :: forall f c a. Decolonnade (Indexed f) c a -> Int
|
||||
maxIndex = go 0 where
|
||||
go :: forall b. Int -> Decolonnade (Indexed f) c b -> Int
|
||||
go !ix (DecolonnadePure _) = ix
|
||||
go !ix1 (DecolonnadeAp (Indexed ix2 _) decode apNext) =
|
||||
go (max ix1 ix2) apNext
|
||||
|
||||
-- | This function uses 'unsafeIndex' to access
|
||||
-- elements of the 'Vector'.
|
||||
uncheckedRunWithRow ::
|
||||
Int
|
||||
-> Decolonnade (Indexed f) content a
|
||||
-> Vector content
|
||||
-> Either (DecolonnadeRowError f content) a
|
||||
uncheckedRunWithRow i d v = mapLeft (DecolonnadeRowError i . RowErrorDecode) (uncheckedRun d v)
|
||||
|
||||
-- | This function does not check to make sure that the indicies in
|
||||
-- the 'Decolonnade' are in the 'Vector'.
|
||||
uncheckedRun :: forall content a f.
|
||||
Decolonnade (Indexed f) content a
|
||||
-> Vector content
|
||||
-> Either (DecolonnadeCellErrors f content) a
|
||||
uncheckedRun dc v = getEitherWrap (go dc)
|
||||
where
|
||||
go :: forall b.
|
||||
Decolonnade (Indexed f) content b
|
||||
-> EitherWrap (DecolonnadeCellErrors f content) b
|
||||
go (DecolonnadePure b) = EitherWrap (Right b)
|
||||
go (DecolonnadeAp ixed@(Indexed ix h) decode apNext) =
|
||||
let rnext = go apNext
|
||||
content = Vector.unsafeIndex v ix
|
||||
rcurrent = mapLeft (DecolonnadeCellErrors . Vector.singleton . DecolonnadeCellError content ixed) (decode content)
|
||||
in rnext <*> (EitherWrap rcurrent)
|
||||
|
||||
headlessToIndexed :: forall c a.
|
||||
Decolonnade Headless c a -> Decolonnade (Indexed Headless) c a
|
||||
headlessToIndexed = go 0 where
|
||||
go :: forall b. Int -> Decolonnade Headless c b -> Decolonnade (Indexed Headless) c b
|
||||
go !ix (DecolonnadePure a) = DecolonnadePure a
|
||||
go !ix (DecolonnadeAp Headless decode apNext) =
|
||||
DecolonnadeAp (Indexed ix Headless) decode (go (ix + 1) apNext)
|
||||
|
||||
length :: forall f c a. Decolonnade f c a -> Int
|
||||
length = go 0 where
|
||||
go :: forall b. Int -> Decolonnade f c b -> Int
|
||||
go !a (DecolonnadePure _) = a
|
||||
go !a (DecolonnadeAp _ _ apNext) = go (a + 1) apNext
|
||||
|
||||
-- | Maps over a 'Decolonnade' that expects headers, converting these
|
||||
-- expected headers into the indices of the columns that they
|
||||
-- correspond to.
|
||||
headedToIndexed :: forall content a. Eq content
|
||||
=> Vector content -- ^ Headers in the source document
|
||||
-> Decolonnade Headed content a -- ^ Decolonnade that contains expected headers
|
||||
-> Either (HeadingErrors content) (Decolonnade (Indexed Headed) content a)
|
||||
headedToIndexed v = getEitherWrap . go
|
||||
where
|
||||
go :: forall b. Eq content
|
||||
=> Decolonnade Headed content b
|
||||
-> EitherWrap (HeadingErrors content) (Decolonnade (Indexed Headed) content b)
|
||||
go (DecolonnadePure b) = EitherWrap (Right (DecolonnadePure b))
|
||||
go (DecolonnadeAp hd@(Headed h) decode apNext) =
|
||||
let rnext = go apNext
|
||||
ixs = Vector.elemIndices h v
|
||||
ixsLen = Vector.length ixs
|
||||
rcurrent
|
||||
| ixsLen == 1 = Right (Vector.unsafeIndex ixs 0)
|
||||
| ixsLen == 0 = Left (HeadingErrors (Vector.singleton h) Vector.empty)
|
||||
| otherwise = Left (HeadingErrors Vector.empty (Vector.singleton (h,ixsLen)))
|
||||
in (\ix ap -> DecolonnadeAp (Indexed ix hd) decode ap)
|
||||
<$> EitherWrap rcurrent
|
||||
<*> rnext
|
||||
|
||||
-- | This adds one to the index because text editors consider
|
||||
-- line number to be one-based, not zero-based.
|
||||
prettyError :: (c -> String) -> DecolonnadeRowError f c -> String
|
||||
prettyError toStr (DecolonnadeRowError ix e) = unlines
|
||||
$ ("Decolonnade error on line " ++ show (ix + 1) ++ " of file.")
|
||||
: ("Error Category: " ++ descr)
|
||||
: map (" " ++) errDescrs
|
||||
where (descr,errDescrs) = prettyRowError toStr e
|
||||
|
||||
prettyRowError :: (content -> String) -> RowError f content -> (String, [String])
|
||||
prettyRowError toStr x = case x of
|
||||
RowErrorParse err -> (,) "CSV Parsing"
|
||||
[ "The line could not be parsed into cells correctly."
|
||||
, "Original parser error: " ++ err
|
||||
]
|
||||
RowErrorSize reqLen actualLen -> (,) "Row Length"
|
||||
[ "Expected the row to have exactly " ++ show reqLen ++ " cells."
|
||||
, "The row only has " ++ show actualLen ++ " cells."
|
||||
]
|
||||
RowErrorMinSize reqLen actualLen -> (,) "Row Min Length"
|
||||
[ "Expected the row to have at least " ++ show reqLen ++ " cells."
|
||||
, "The row only has " ++ show actualLen ++ " cells."
|
||||
]
|
||||
RowErrorMalformed enc -> (,) "Text Decolonnade"
|
||||
[ "Tried to decode the input as " ++ enc ++ " text"
|
||||
, "There is a mistake in the encoding of the text."
|
||||
]
|
||||
RowErrorHeading errs -> (,) "Header" (prettyHeadingErrors toStr errs)
|
||||
RowErrorDecode errs -> (,) "Cell Decolonnade" (prettyCellErrors toStr errs)
|
||||
|
||||
prettyCellErrors :: (c -> String) -> DecolonnadeCellErrors f c -> [String]
|
||||
prettyCellErrors toStr (DecolonnadeCellErrors errs) = drop 1 $
|
||||
flip concatMap errs $ \(DecolonnadeCellError content (Indexed ix _) msg) ->
|
||||
let str = toStr content in
|
||||
[ "-----------"
|
||||
, "Column " ++ columnNumToLetters ix
|
||||
, "Original parse error: " ++ msg
|
||||
, "Cell Content Length: " ++ show (Prelude.length str)
|
||||
, "Cell Content: " ++ if null str
|
||||
then "[empty cell]"
|
||||
else str
|
||||
]
|
||||
|
||||
prettyHeadingErrors :: (c -> String) -> HeadingErrors c -> [String]
|
||||
prettyHeadingErrors conv (HeadingErrors missing duplicates) = concat
|
||||
[ concatMap (\h -> ["The header " ++ conv h ++ " was missing."]) missing
|
||||
, concatMap (\(h,n) -> ["The header " ++ conv h ++ " occurred " ++ show n ++ " times."]) duplicates
|
||||
]
|
||||
|
||||
columnNumToLetters :: Int -> String
|
||||
columnNumToLetters i
|
||||
| i >= 0 && i < 25 = [chr (i + 65)]
|
||||
| otherwise = "Beyond Z. Fix this."
|
||||
|
||||
|
||||
newtype EitherWrap a b = EitherWrap
|
||||
{ getEitherWrap :: Either a b
|
||||
} deriving (Functor)
|
||||
|
||||
instance Monoid a => Applicative (EitherWrap a) where
|
||||
pure = EitherWrap . Right
|
||||
EitherWrap (Left a1) <*> EitherWrap (Left a2) = EitherWrap (Left (mappend a1 a2))
|
||||
EitherWrap (Left a1) <*> EitherWrap (Right _) = EitherWrap (Left a1)
|
||||
EitherWrap (Right _) <*> EitherWrap (Left a2) = EitherWrap (Left a2)
|
||||
EitherWrap (Right f) <*> EitherWrap (Right b) = EitherWrap (Right (f b))
|
||||
|
||||
mapLeft :: (a -> b) -> Either a c -> Either b c
|
||||
mapLeft _ (Right a) = Right a
|
||||
mapLeft f (Left a) = Left (f a)
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveFoldable #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
{-# OPTIONS_HADDOCK not-home #-}
|
||||
{-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
-- | Most users of this library do not need this module. The functions
|
||||
-- here are used to build functions that apply a 'Colonnade'
|
||||
-- to a collection of values, building a table from them. Ultimately,
|
||||
@ -25,12 +37,24 @@
|
||||
-- an @a@ value since a value is not needed to build a header.
|
||||
--
|
||||
module Colonnade.Encode
|
||||
( row
|
||||
( -- * Colonnade
|
||||
-- ** Types
|
||||
Colonnade(..)
|
||||
, OneColonnade(..)
|
||||
, Headed(..)
|
||||
, Headless(..)
|
||||
, Sized(..)
|
||||
, ExtractForall(..)
|
||||
-- ** Typeclasses
|
||||
, Headedness(..)
|
||||
-- ** Row
|
||||
, row
|
||||
, rowMonadic
|
||||
, rowMonadic_
|
||||
, rowMonadicWith
|
||||
, rowMonoidal
|
||||
, rowMonoidalHeader
|
||||
-- ** Header
|
||||
, header
|
||||
, headerMonadic
|
||||
, headerMonadic_
|
||||
@ -38,23 +62,43 @@ module Colonnade.Encode
|
||||
, headerMonadicGeneral_
|
||||
, headerMonoidalGeneral
|
||||
, headerMonoidalFull
|
||||
-- ** Other
|
||||
, bothMonadic_
|
||||
, freezeMutableSizedColonnade
|
||||
, newMutableSizedColonnade
|
||||
, rowUpdateSize
|
||||
, headerUpdateSize
|
||||
, sizeColumns
|
||||
-- * Cornice
|
||||
-- ** Types
|
||||
, Cornice(..)
|
||||
, AnnotatedCornice(..)
|
||||
, OneCornice(..)
|
||||
, Pillar(..)
|
||||
, ToEmptyCornice(..)
|
||||
, Fascia(..)
|
||||
-- ** Encoding
|
||||
, annotate
|
||||
, annotateFinely
|
||||
, size
|
||||
, endow
|
||||
, discard
|
||||
, headersMonoidal
|
||||
, uncapAnnotated
|
||||
) where
|
||||
|
||||
import Colonnade.Internal
|
||||
import Data.Vector (Vector)
|
||||
import Data.Foldable
|
||||
import Control.Monad.ST (ST,runST)
|
||||
import Data.Monoid
|
||||
import Data.Functor.Contravariant (Contravariant(..))
|
||||
import Data.Profunctor (Profunctor(..))
|
||||
import Data.Semigroup (Semigroup)
|
||||
import Data.List.NonEmpty (NonEmpty((:|)))
|
||||
import Data.Foldable (toList)
|
||||
import qualified Data.Semigroup as Semigroup
|
||||
import qualified Data.Vector as Vector
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.Vector.Unboxed.Mutable as MVU
|
||||
import qualified Data.Vector.Unboxed as VU
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.Vector as Vector
|
||||
import qualified Data.Vector.Generic as GV
|
||||
|
||||
-- | Consider providing a variant the produces a list
|
||||
@ -98,7 +142,7 @@ rowMonoidal ::
|
||||
-> a
|
||||
-> m
|
||||
rowMonoidal (Colonnade v) g a =
|
||||
foldMap (\(OneColonnade h encode) -> g (encode a)) v
|
||||
foldMap (\(OneColonnade _ encode) -> g (encode a)) v
|
||||
|
||||
rowMonoidalHeader ::
|
||||
Monoid m
|
||||
@ -134,7 +178,7 @@ sizeColumns :: (Foldable f, Foldable h)
|
||||
=> (c -> Int) -- ^ Get size from content
|
||||
-> f a
|
||||
-> Colonnade h a c
|
||||
-> Colonnade (Sized h) a c
|
||||
-> Colonnade (Sized (Maybe Int) h) a c
|
||||
sizeColumns toSize rows colonnade = runST $ do
|
||||
mcol <- newMutableSizedColonnade colonnade
|
||||
headerUpdateSize toSize mcol
|
||||
@ -146,14 +190,14 @@ newMutableSizedColonnade (Colonnade v) = do
|
||||
mv <- MVU.replicate (V.length v) 0
|
||||
return (MutableSizedColonnade v mv)
|
||||
|
||||
freezeMutableSizedColonnade :: MutableSizedColonnade s h a c -> ST s (Colonnade (Sized h) a c)
|
||||
freezeMutableSizedColonnade :: MutableSizedColonnade s h a c -> ST s (Colonnade (Sized (Maybe Int) h) a c)
|
||||
freezeMutableSizedColonnade (MutableSizedColonnade v mv) =
|
||||
if MVU.length mv /= V.length v
|
||||
then error "rowMonoidalSize: vector sizes mismatched"
|
||||
else do
|
||||
sizeVec <- VU.freeze mv
|
||||
return $ Colonnade
|
||||
$ V.map (\(OneColonnade h enc,sz) -> OneColonnade (Sized sz h) enc)
|
||||
$ V.map (\(OneColonnade h enc,sz) -> OneColonnade (Sized (Just sz) h) enc)
|
||||
$ V.zip v (GV.convert sizeVec)
|
||||
|
||||
rowMonadicWith ::
|
||||
@ -193,12 +237,13 @@ headerMonadic (Colonnade v) g =
|
||||
fmap (mconcat . Vector.toList) $ Vector.mapM (g . getHeaded . oneColonnadeHead) v
|
||||
|
||||
headerMonadicGeneral_ ::
|
||||
(Monad m, Foldable h)
|
||||
(Monad m, Headedness h)
|
||||
=> Colonnade h a c
|
||||
-> (c -> m b)
|
||||
-> m ()
|
||||
headerMonadicGeneral_ (Colonnade v) g =
|
||||
Vector.mapM_ (mapM_ g . oneColonnadeHead) v
|
||||
headerMonadicGeneral_ (Colonnade v) g = case headednessExtract of
|
||||
Nothing -> return ()
|
||||
Just f -> Vector.mapM_ (g . f . oneColonnadeHead) v
|
||||
|
||||
headerMonoidalGeneral ::
|
||||
(Monoid m, Foldable h)
|
||||
@ -225,4 +270,422 @@ headerMonadic_ (Colonnade v) g = Vector.mapM_ (g . getHeaded . oneColonnadeHead)
|
||||
foldlMapM :: (Foldable t, Monoid b, Monad m) => (a -> m b) -> t a -> m b
|
||||
foldlMapM f = foldlM (\b a -> fmap (mappend b) (f a)) mempty
|
||||
|
||||
discard :: Cornice h p a c -> Colonnade h a c
|
||||
discard = go where
|
||||
go :: forall h p a c. Cornice h p a c -> Colonnade h a c
|
||||
go (CorniceBase c) = c
|
||||
go (CorniceCap children) = Colonnade (getColonnade . go . oneCorniceBody =<< children)
|
||||
|
||||
endow :: forall p a c. (c -> c -> c) -> Cornice Headed p a c -> Colonnade Headed a c
|
||||
endow f x = case x of
|
||||
CorniceBase colonnade -> colonnade
|
||||
CorniceCap v -> Colonnade (V.concatMap (\(OneCornice h b) -> go h b) v)
|
||||
where
|
||||
go :: forall p'. c -> Cornice Headed p' a c -> Vector (OneColonnade Headed a c)
|
||||
go c (CorniceBase (Colonnade v)) = V.map (mapOneColonnadeHeader (f c)) v
|
||||
go c (CorniceCap v) = V.concatMap (\(OneCornice h b) -> go (f c h) b) v
|
||||
|
||||
uncapAnnotated :: forall sz p a c h.
|
||||
AnnotatedCornice sz h p a c
|
||||
-> Colonnade (Sized sz h) a c
|
||||
uncapAnnotated x = case x of
|
||||
AnnotatedCorniceBase _ colonnade -> colonnade
|
||||
AnnotatedCorniceCap _ v -> Colonnade (V.concatMap (\(OneCornice _ b) -> go b) v)
|
||||
where
|
||||
go :: forall p'.
|
||||
AnnotatedCornice sz h p' a c
|
||||
-> Vector (OneColonnade (Sized sz h) a c)
|
||||
go (AnnotatedCorniceBase _ (Colonnade v)) = v
|
||||
go (AnnotatedCorniceCap _ v) = V.concatMap (\(OneCornice _ b) -> go b) v
|
||||
|
||||
annotate :: Cornice Headed p a c -> AnnotatedCornice (Maybe Int) Headed p a c
|
||||
annotate = go where
|
||||
go :: forall p a c. Cornice Headed p a c -> AnnotatedCornice (Maybe Int) Headed p a c
|
||||
go (CorniceBase c) = let len = V.length (getColonnade c) in
|
||||
AnnotatedCorniceBase
|
||||
(if len > 0 then (Just len) else Nothing)
|
||||
(mapHeadedness (Sized (Just 1)) c)
|
||||
go (CorniceCap children) =
|
||||
let annChildren = fmap (mapOneCorniceBody go) children
|
||||
in AnnotatedCorniceCap
|
||||
( ( ( V.foldl' (combineJustInt (+))
|
||||
) Nothing . V.map (size . oneCorniceBody)
|
||||
) annChildren
|
||||
)
|
||||
annChildren
|
||||
|
||||
combineJustInt :: (Int -> Int -> Int) -> Maybe Int -> Maybe Int -> Maybe Int
|
||||
combineJustInt f acc el = case acc of
|
||||
Nothing -> case el of
|
||||
Nothing -> Nothing
|
||||
Just i -> Just i
|
||||
Just i -> case el of
|
||||
Nothing -> Just i
|
||||
Just j -> Just (f i j)
|
||||
|
||||
mapJustInt :: (Int -> Int) -> Maybe Int -> Maybe Int
|
||||
mapJustInt _ Nothing = Nothing
|
||||
mapJustInt f (Just i) = Just (f i)
|
||||
|
||||
annotateFinely :: Foldable f
|
||||
=> (Int -> Int -> Int) -- ^ fold function
|
||||
-> (Int -> Int) -- ^ finalize
|
||||
-> (c -> Int) -- ^ Get size from content
|
||||
-> f a
|
||||
-> Cornice Headed p a c
|
||||
-> AnnotatedCornice (Maybe Int) Headed p a c
|
||||
annotateFinely g finish toSize xs cornice = runST $ do
|
||||
m <- newMutableSizedCornice cornice
|
||||
sizeColonnades toSize xs m
|
||||
freezeMutableSizedCornice g finish m
|
||||
|
||||
sizeColonnades :: forall f s p a c.
|
||||
Foldable f
|
||||
=> (c -> Int) -- ^ Get size from content
|
||||
-> f a
|
||||
-> MutableSizedCornice s p a c
|
||||
-> ST s ()
|
||||
sizeColonnades toSize xs cornice = do
|
||||
goHeader cornice
|
||||
mapM_ (goRow cornice) xs
|
||||
where
|
||||
goRow :: forall p'. MutableSizedCornice s p' a c -> a -> ST s ()
|
||||
goRow (MutableSizedCorniceBase c) a = rowUpdateSize toSize c a
|
||||
goRow (MutableSizedCorniceCap children) a = mapM_ (flip goRow a . oneCorniceBody) children
|
||||
goHeader :: forall p'. MutableSizedCornice s p' a c -> ST s ()
|
||||
goHeader (MutableSizedCorniceBase c) = headerUpdateSize toSize c
|
||||
goHeader (MutableSizedCorniceCap children) = mapM_ (goHeader . oneCorniceBody) children
|
||||
|
||||
freezeMutableSizedCornice :: forall s p a c.
|
||||
(Int -> Int -> Int) -- ^ fold function
|
||||
-> (Int -> Int) -- ^ finalize
|
||||
-> MutableSizedCornice s p a c
|
||||
-> ST s (AnnotatedCornice (Maybe Int) Headed p a c)
|
||||
freezeMutableSizedCornice step finish = go
|
||||
where
|
||||
go :: forall p' a' c'.
|
||||
MutableSizedCornice s p' a' c'
|
||||
-> ST s (AnnotatedCornice (Maybe Int) Headed p' a' c')
|
||||
go (MutableSizedCorniceBase msc) = do
|
||||
szCol <- freezeMutableSizedColonnade msc
|
||||
let sz =
|
||||
( mapJustInt finish
|
||||
. V.foldl' (combineJustInt step) Nothing
|
||||
. V.map (sizedSize . oneColonnadeHead)
|
||||
) (getColonnade szCol)
|
||||
return (AnnotatedCorniceBase sz szCol)
|
||||
go (MutableSizedCorniceCap v1) = do
|
||||
v2 <- V.mapM (traverseOneCorniceBody go) v1
|
||||
let sz =
|
||||
( mapJustInt finish
|
||||
. V.foldl' (combineJustInt step) Nothing
|
||||
. V.map (size . oneCorniceBody)
|
||||
) v2
|
||||
return $ AnnotatedCorniceCap sz v2
|
||||
|
||||
newMutableSizedCornice :: forall s p a c.
|
||||
Cornice Headed p a c
|
||||
-> ST s (MutableSizedCornice s p a c)
|
||||
newMutableSizedCornice = go where
|
||||
go :: forall p'. Cornice Headed p' a c -> ST s (MutableSizedCornice s p' a c)
|
||||
go (CorniceBase c) = fmap MutableSizedCorniceBase (newMutableSizedColonnade c)
|
||||
go (CorniceCap v) = fmap MutableSizedCorniceCap (V.mapM (traverseOneCorniceBody go) v)
|
||||
|
||||
traverseOneCorniceBody :: Monad m => (k p a c -> m (j p a c)) -> OneCornice k p a c -> m (OneCornice j p a c)
|
||||
traverseOneCorniceBody f (OneCornice h b) = fmap (OneCornice h) (f b)
|
||||
|
||||
mapHeadedness :: (forall x. h x -> h' x) -> Colonnade h a c -> Colonnade h' a c
|
||||
mapHeadedness f (Colonnade v) =
|
||||
Colonnade (V.map (\(OneColonnade h c) -> OneColonnade (f h) c) v)
|
||||
|
||||
|
||||
-- | This is an O(1) operation, sort of
|
||||
size :: AnnotatedCornice sz h p a c -> sz
|
||||
size x = case x of
|
||||
AnnotatedCorniceBase m _ -> m
|
||||
AnnotatedCorniceCap sz _ -> sz
|
||||
|
||||
mapOneCorniceBody :: (forall p' a' c'. k p' a' c' -> j p' a' c') -> OneCornice k p a c -> OneCornice j p a c
|
||||
mapOneCorniceBody f (OneCornice h b) = OneCornice h (f b)
|
||||
|
||||
mapOneColonnadeHeader :: Functor h => (c -> c) -> OneColonnade h a c -> OneColonnade h a c
|
||||
mapOneColonnadeHeader f (OneColonnade h b) = OneColonnade (fmap f h) b
|
||||
|
||||
headersMonoidal :: forall sz r m c p a h.
|
||||
(Monoid m, Headedness h)
|
||||
=> Maybe (Fascia p r, r -> m -> m) -- ^ Apply the Fascia header row content
|
||||
-> [(sz -> c -> m, m -> m)] -- ^ Build content from cell content and size
|
||||
-> AnnotatedCornice sz h p a c
|
||||
-> m
|
||||
headersMonoidal wrapRow fromContentList = go wrapRow
|
||||
where
|
||||
go :: forall p'. Maybe (Fascia p' r, r -> m -> m) -> AnnotatedCornice sz h p' a c -> m
|
||||
go ef (AnnotatedCorniceBase _ (Colonnade v)) =
|
||||
let g :: m -> m
|
||||
g m = case ef of
|
||||
Nothing -> m
|
||||
Just (FasciaBase r, f) -> f r m
|
||||
in case headednessExtract of
|
||||
Just unhead -> g $ foldMap (\(fromContent,wrap) -> wrap
|
||||
(foldMap (\(OneColonnade (Sized sz h) _) ->
|
||||
(fromContent sz (unhead h))) v)) fromContentList
|
||||
Nothing -> mempty
|
||||
go ef (AnnotatedCorniceCap _ v) =
|
||||
let g :: m -> m
|
||||
g m = case ef of
|
||||
Nothing -> m
|
||||
Just (FasciaCap r _, f) -> f r m
|
||||
in g (foldMap (\(fromContent,wrap) -> wrap (foldMap (\(OneCornice h b) ->
|
||||
(fromContent (size b) h)) v)) fromContentList)
|
||||
<> case ef of
|
||||
Nothing -> case flattenAnnotated v of
|
||||
Nothing -> mempty
|
||||
Just annCoreNext -> go Nothing annCoreNext
|
||||
Just (FasciaCap _ fn, f) -> case flattenAnnotated v of
|
||||
Nothing -> mempty
|
||||
Just annCoreNext -> go (Just (fn,f)) annCoreNext
|
||||
|
||||
flattenAnnotated ::
|
||||
Vector (OneCornice (AnnotatedCornice sz h) p a c)
|
||||
-> Maybe (AnnotatedCornice sz h p a c)
|
||||
flattenAnnotated v = case v V.!? 0 of
|
||||
Nothing -> Nothing
|
||||
Just (OneCornice _ x) -> Just $ case x of
|
||||
AnnotatedCorniceBase m _ -> flattenAnnotatedBase m v
|
||||
AnnotatedCorniceCap m _ -> flattenAnnotatedCap m v
|
||||
|
||||
flattenAnnotatedBase ::
|
||||
sz
|
||||
-> Vector (OneCornice (AnnotatedCornice sz h) Base a c)
|
||||
-> AnnotatedCornice sz h Base a c
|
||||
flattenAnnotatedBase msz = AnnotatedCorniceBase msz
|
||||
. Colonnade
|
||||
. V.concatMap
|
||||
(\(OneCornice _ (AnnotatedCorniceBase _ (Colonnade v))) -> v)
|
||||
|
||||
flattenAnnotatedCap ::
|
||||
sz
|
||||
-> Vector (OneCornice (AnnotatedCornice sz h) (Cap p) a c)
|
||||
-> AnnotatedCornice sz h (Cap p) a c
|
||||
flattenAnnotatedCap m = AnnotatedCorniceCap m . V.concatMap getTheVector
|
||||
|
||||
getTheVector ::
|
||||
OneCornice (AnnotatedCornice sz h) (Cap p) a c
|
||||
-> Vector (OneCornice (AnnotatedCornice sz h) p a c)
|
||||
getTheVector (OneCornice _ (AnnotatedCorniceCap _ v)) = v
|
||||
|
||||
data MutableSizedCornice s (p :: Pillar) a c where
|
||||
MutableSizedCorniceBase ::
|
||||
{-# UNPACK #-} !(MutableSizedColonnade s Headed a c)
|
||||
-> MutableSizedCornice s Base a c
|
||||
MutableSizedCorniceCap ::
|
||||
{-# UNPACK #-} !(Vector (OneCornice (MutableSizedCornice s) p a c))
|
||||
-> MutableSizedCornice s (Cap p) a c
|
||||
|
||||
data MutableSizedColonnade s h a c = MutableSizedColonnade
|
||||
{ _mutableSizedColonnadeColumns :: {-# UNPACK #-} !(Vector (OneColonnade h a c))
|
||||
, _mutableSizedColonnadeSizes :: {-# UNPACK #-} !(MVU.STVector s Int)
|
||||
}
|
||||
|
||||
-- | As the first argument to the 'Colonnade' type
|
||||
-- constructor, this indictates that the columnar encoding has
|
||||
-- a header. This type is isomorphic to 'Identity' but is
|
||||
-- given a new name to clarify its intent:
|
||||
--
|
||||
-- > example :: Colonnade Headed Foo Text
|
||||
--
|
||||
-- The term @example@ represents a columnar encoding of @Foo@
|
||||
-- in which the columns have headings.
|
||||
newtype Headed a = Headed { getHeaded :: a }
|
||||
deriving (Eq,Ord,Functor,Show,Read,Foldable)
|
||||
|
||||
instance Applicative Headed where
|
||||
pure = Headed
|
||||
Headed f <*> Headed a = Headed (f a)
|
||||
|
||||
-- | As the first argument to the 'Colonnade' type
|
||||
-- constructor, this indictates that the columnar encoding does not have
|
||||
-- a header. This type is isomorphic to 'Proxy' but is
|
||||
-- given a new name to clarify its intent:
|
||||
--
|
||||
-- > example :: Colonnade Headless Foo Text
|
||||
--
|
||||
-- The term @example@ represents a columnar encoding of @Foo@
|
||||
-- in which the columns do not have headings.
|
||||
data Headless a = Headless
|
||||
deriving (Eq,Ord,Functor,Show,Read,Foldable)
|
||||
|
||||
instance Applicative Headless where
|
||||
pure _ = Headless
|
||||
Headless <*> Headless = Headless
|
||||
|
||||
data Sized sz f a = Sized
|
||||
{ sizedSize :: !sz
|
||||
, sizedContent :: !(f a)
|
||||
} deriving (Functor, Foldable)
|
||||
|
||||
instance Contravariant Headless where
|
||||
contramap _ Headless = Headless
|
||||
|
||||
-- | Encodes a header and a cell.
|
||||
data OneColonnade h a c = OneColonnade
|
||||
{ oneColonnadeHead :: !(h c)
|
||||
, oneColonnadeEncode :: !(a -> c)
|
||||
} deriving (Functor)
|
||||
|
||||
instance Functor h => Profunctor (OneColonnade h) where
|
||||
rmap = fmap
|
||||
lmap f (OneColonnade h e) = OneColonnade h (e . f)
|
||||
|
||||
-- | An columnar encoding of @a@. The type variable @h@ determines what
|
||||
-- is present in each column in the header row. It is typically instantiated
|
||||
-- to 'Headed' and occasionally to 'Headless'. There is nothing that
|
||||
-- restricts it to these two types, although they satisfy the majority
|
||||
-- of use cases. The type variable @c@ is the content type. This can
|
||||
-- be @Text@, @String@, or @ByteString@. In the companion libraries
|
||||
-- @reflex-dom-colonnade@ and @yesod-colonnade@, additional types
|
||||
-- that represent HTML with element attributes are provided that serve
|
||||
-- as the content type. Presented more visually:
|
||||
--
|
||||
-- > +---- Value consumed to build a row
|
||||
-- > |
|
||||
-- > v
|
||||
-- > Colonnade h a c
|
||||
-- > ^ ^
|
||||
-- > | |
|
||||
-- > | +-- Content (Text, ByteString, Html, etc.)
|
||||
-- > |
|
||||
-- > +------ Headedness (Headed or Headless)
|
||||
--
|
||||
-- Internally, a 'Colonnade' is represented as a 'Vector' of individual
|
||||
-- column encodings. It is possible to use any collection type with
|
||||
-- 'Alternative' and 'Foldable' instances. However, 'Vector' was chosen to
|
||||
-- optimize the data structure for the use case of building the structure
|
||||
-- once and then folding over it many times. It is recommended that
|
||||
-- 'Colonnade's are defined at the top-level so that GHC avoids reconstructing
|
||||
-- them every time they are used.
|
||||
newtype Colonnade h a c = Colonnade
|
||||
{ getColonnade :: Vector (OneColonnade h a c)
|
||||
} deriving (Monoid,Functor)
|
||||
|
||||
instance Functor h => Profunctor (Colonnade h) where
|
||||
rmap = fmap
|
||||
lmap f (Colonnade v) = Colonnade (Vector.map (lmap f) v)
|
||||
|
||||
instance Semigroup (Colonnade h a c) where
|
||||
Colonnade a <> Colonnade b = Colonnade (a Vector.++ b)
|
||||
sconcat xs = Colonnade (vectorConcatNE (fmap getColonnade xs))
|
||||
|
||||
-- | Isomorphic to the natural numbers. Only the promoted version of
|
||||
-- this type is used.
|
||||
data Pillar = Cap !Pillar | Base
|
||||
|
||||
class ToEmptyCornice (p :: Pillar) where
|
||||
toEmptyCornice :: Cornice h p a c
|
||||
|
||||
instance ToEmptyCornice Base where
|
||||
toEmptyCornice = CorniceBase mempty
|
||||
|
||||
instance ToEmptyCornice (Cap p) where
|
||||
toEmptyCornice = CorniceCap Vector.empty
|
||||
|
||||
data Fascia (p :: Pillar) r where
|
||||
FasciaBase :: !r -> Fascia Base r
|
||||
FasciaCap :: !r -> Fascia p r -> Fascia (Cap p) r
|
||||
|
||||
data OneCornice k (p :: Pillar) a c = OneCornice
|
||||
{ oneCorniceHead :: !c
|
||||
, oneCorniceBody :: !(k p a c)
|
||||
} deriving (Functor)
|
||||
|
||||
data Cornice h (p :: Pillar) a c where
|
||||
CorniceBase :: !(Colonnade h a c) -> Cornice h Base a c
|
||||
CorniceCap :: {-# UNPACK #-} !(Vector (OneCornice (Cornice h) p a c)) -> Cornice h (Cap p) a c
|
||||
|
||||
instance Functor h => Functor (Cornice h p a) where
|
||||
fmap f x = case x of
|
||||
CorniceBase c -> CorniceBase (fmap f c)
|
||||
CorniceCap c -> CorniceCap (mapVectorCornice f c)
|
||||
|
||||
instance Functor h => Profunctor (Cornice h p) where
|
||||
rmap = fmap
|
||||
lmap f x = case x of
|
||||
CorniceBase c -> CorniceBase (lmap f c)
|
||||
CorniceCap c -> CorniceCap (contramapVectorCornice f c)
|
||||
|
||||
instance Semigroup (Cornice h p a c) where
|
||||
CorniceBase a <> CorniceBase b = CorniceBase (mappend a b)
|
||||
CorniceCap a <> CorniceCap b = CorniceCap (a Vector.++ b)
|
||||
sconcat xs@(x :| _) = case x of
|
||||
CorniceBase _ -> CorniceBase (Colonnade (vectorConcatNE (fmap (getColonnade . getCorniceBase) xs)))
|
||||
CorniceCap _ -> CorniceCap (vectorConcatNE (fmap getCorniceCap xs))
|
||||
|
||||
instance ToEmptyCornice p => Monoid (Cornice h p a c) where
|
||||
mempty = toEmptyCornice
|
||||
mappend = (Semigroup.<>)
|
||||
mconcat xs1 = case xs1 of
|
||||
[] -> toEmptyCornice
|
||||
x : xs2 -> Semigroup.sconcat (x :| xs2)
|
||||
|
||||
mapVectorCornice :: Functor h => (c -> d) -> Vector (OneCornice (Cornice h) p a c) -> Vector (OneCornice (Cornice h) p a d)
|
||||
mapVectorCornice f = V.map (fmap f)
|
||||
|
||||
contramapVectorCornice :: Functor h => (b -> a) -> Vector (OneCornice (Cornice h) p a c) -> Vector (OneCornice (Cornice h) p b c)
|
||||
contramapVectorCornice f = V.map (lmapOneCornice f)
|
||||
|
||||
lmapOneCornice :: Functor h => (b -> a) -> OneCornice (Cornice h) p a c -> OneCornice (Cornice h) p b c
|
||||
lmapOneCornice f (OneCornice theHead theBody) = OneCornice theHead (lmap f theBody)
|
||||
|
||||
getCorniceBase :: Cornice h Base a c -> Colonnade h a c
|
||||
getCorniceBase (CorniceBase c) = c
|
||||
|
||||
getCorniceCap :: Cornice h (Cap p) a c -> Vector (OneCornice (Cornice h) p a c)
|
||||
getCorniceCap (CorniceCap c) = c
|
||||
|
||||
data AnnotatedCornice sz h (p :: Pillar) a c where
|
||||
AnnotatedCorniceBase ::
|
||||
!sz
|
||||
-> !(Colonnade (Sized sz h) a c)
|
||||
-> AnnotatedCornice sz h Base a c
|
||||
AnnotatedCorniceCap ::
|
||||
!sz
|
||||
-> {-# UNPACK #-} !(Vector (OneCornice (AnnotatedCornice sz h) p a c))
|
||||
-> AnnotatedCornice sz h (Cap p) a c
|
||||
|
||||
-- data MaybeInt = JustInt {-# UNPACK #-} !Int | NothingInt
|
||||
|
||||
-- | This is provided with @vector-0.12@, but we include a copy here
|
||||
-- for compatibility.
|
||||
vectorConcatNE :: NonEmpty (Vector a) -> Vector a
|
||||
vectorConcatNE = Vector.concat . toList
|
||||
|
||||
-- | This class communicates that a container holds either zero
|
||||
-- elements or one element. Furthermore, all inhabitants of
|
||||
-- the type must hold the same number of elements. Both
|
||||
-- 'Headed' and 'Headless' have instances. The following
|
||||
-- law accompanies any instances:
|
||||
--
|
||||
-- > maybe x (\f -> f (headednessPure x)) headednessContents == x
|
||||
-- > todo: come up with another law that relates to Traversable
|
||||
--
|
||||
-- Consequently, there is no instance for 'Maybe', which cannot
|
||||
-- satisfy the laws since it has inhabitants which hold different
|
||||
-- numbers of elements. 'Nothing' holds 0 elements and 'Just' holds
|
||||
-- 1 element.
|
||||
class Headedness h where
|
||||
headednessPure :: a -> h a
|
||||
headednessExtract :: Maybe (h a -> a)
|
||||
headednessExtractForall :: Maybe (ExtractForall h)
|
||||
|
||||
instance Headedness Headed where
|
||||
headednessPure = Headed
|
||||
headednessExtract = Just getHeaded
|
||||
headednessExtractForall = Just (ExtractForall getHeaded)
|
||||
|
||||
instance Headedness Headless where
|
||||
headednessPure _ = Headless
|
||||
headednessExtract = Nothing
|
||||
headednessExtractForall = Nothing
|
||||
|
||||
newtype ExtractForall h = ExtractForall { runExtractForall :: forall a. h a -> a }
|
||||
|
||||
|
||||
@ -1,197 +0,0 @@
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE DeriveFoldable #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
|
||||
{-# OPTIONS_HADDOCK not-home #-}
|
||||
{-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-unticked-promoted-constructors -Werror #-}
|
||||
|
||||
module Colonnade.Internal
|
||||
( -- * Colonnade
|
||||
Colonnade(..)
|
||||
, OneColonnade(..)
|
||||
, Headed(..)
|
||||
, Headless(..)
|
||||
-- * Cornice
|
||||
, Cornice(..)
|
||||
, AnnotatedCornice(..)
|
||||
, OneCornice(..)
|
||||
, Pillar(..)
|
||||
, ToEmptyCornice(..)
|
||||
, Fascia(..)
|
||||
-- * Sizing
|
||||
, Sized(..)
|
||||
, MutableSizedColonnade(..)
|
||||
, MutableSizedCornice(..)
|
||||
) where
|
||||
|
||||
import Data.Vector (Vector)
|
||||
import Data.Functor.Contravariant (Contravariant(..))
|
||||
import Data.Functor.Contravariant.Divisible (Divisible(..))
|
||||
import Control.Exception (Exception)
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Profunctor (Profunctor(..))
|
||||
import Data.Semigroup (Semigroup)
|
||||
import Data.List.NonEmpty (NonEmpty((:|)))
|
||||
import Data.Foldable (toList)
|
||||
import qualified Data.Vector.Unboxed.Mutable as MVU
|
||||
import qualified Data.Semigroup as Semigroup
|
||||
import qualified Data.Vector as Vector
|
||||
import qualified Data.Vector.Generic as VG
|
||||
|
||||
-- | As the first argument to the 'Colonnade' type
|
||||
-- constructor, this indictates that the columnar encoding has
|
||||
-- a header. This type is isomorphic to 'Identity' but is
|
||||
-- given a new name to clarify its intent:
|
||||
--
|
||||
-- > example :: Colonnade Headed Foo Text
|
||||
--
|
||||
-- The term @example@ represents a columnar encoding of @Foo@
|
||||
-- in which the columns have headings.
|
||||
newtype Headed a = Headed { getHeaded :: a }
|
||||
deriving (Eq,Ord,Functor,Show,Read,Foldable)
|
||||
|
||||
-- | As the first argument to the 'Colonnade' type
|
||||
-- constructor, this indictates that the columnar encoding does not have
|
||||
-- a header. This type is isomorphic to 'Proxy' but is
|
||||
-- given a new name to clarify its intent:
|
||||
--
|
||||
-- > example :: Colonnade Headless Foo Text
|
||||
--
|
||||
-- The term @example@ represents a columnar encoding of @Foo@
|
||||
-- in which the columns do not have headings.
|
||||
data Headless a = Headless
|
||||
deriving (Eq,Ord,Functor,Show,Read,Foldable)
|
||||
|
||||
data Sized f a = Sized
|
||||
{ sizedSize :: {-# UNPACK #-} !Int
|
||||
, sizedContent :: !(f a)
|
||||
} deriving (Functor, Foldable)
|
||||
|
||||
instance Contravariant Headless where
|
||||
contramap _ Headless = Headless
|
||||
|
||||
-- | Encodes a header and a cell.
|
||||
data OneColonnade h a c = OneColonnade
|
||||
{ oneColonnadeHead :: !(h c)
|
||||
, oneColonnadeEncode :: !(a -> c)
|
||||
} deriving (Functor)
|
||||
|
||||
instance Functor h => Profunctor (OneColonnade h) where
|
||||
rmap = fmap
|
||||
lmap f (OneColonnade h e) = OneColonnade h (e . f)
|
||||
|
||||
-- | An columnar encoding of @a@. The type variable @h@ determines what
|
||||
-- is present in each column in the header row. It is typically instantiated
|
||||
-- to 'Headed' and occasionally to 'Headless'. There is nothing that
|
||||
-- restricts it to these two types, although they satisfy the majority
|
||||
-- of use cases. The type variable @c@ is the content type. This can
|
||||
-- be @Text@, @String@, or @ByteString@. In the companion libraries
|
||||
-- @reflex-dom-colonnade@ and @yesod-colonnade@, additional types
|
||||
-- that represent HTML with element attributes are provided that serve
|
||||
-- as the content type. Presented more visually:
|
||||
--
|
||||
-- > +---- Value consumed to build a row
|
||||
-- > |
|
||||
-- > v
|
||||
-- > Colonnade h a c
|
||||
-- > ^ ^
|
||||
-- > | |
|
||||
-- > | +-- Content (Text, ByteString, Html, etc.)
|
||||
-- > |
|
||||
-- > +------ Headedness (Headed or Headless)
|
||||
--
|
||||
-- Internally, a 'Colonnade' is represented as a 'Vector' of individual
|
||||
-- column encodings. It is possible to use any collection type with
|
||||
-- 'Alternative' and 'Foldable' instances. However, 'Vector' was chosen to
|
||||
-- optimize the data structure for the use case of building the structure
|
||||
-- once and then folding over it many times. It is recommended that
|
||||
-- 'Colonnade's are defined at the top-level so that GHC avoids reconstructing
|
||||
-- them every time they are used.
|
||||
newtype Colonnade h a c = Colonnade
|
||||
{ getColonnade :: Vector (OneColonnade h a c)
|
||||
} deriving (Monoid,Functor)
|
||||
|
||||
instance Functor h => Profunctor (Colonnade h) where
|
||||
rmap = fmap
|
||||
lmap f (Colonnade v) = Colonnade (Vector.map (lmap f) v)
|
||||
|
||||
instance Semigroup (Colonnade h a c) where
|
||||
Colonnade a <> Colonnade b = Colonnade (a Vector.++ b)
|
||||
sconcat xs = Colonnade (vectorConcatNE (fmap getColonnade xs))
|
||||
|
||||
data MutableSizedColonnade s h a c = MutableSizedColonnade
|
||||
{ mutableSizedColonnadeColumns :: {-# UNPACK #-} !(Vector (OneColonnade h a c))
|
||||
, mutableSizedColonnadeSizes :: {-# UNPACK #-} !(MVU.STVector s Int)
|
||||
}
|
||||
|
||||
-- | Isomorphic to the natural numbers. Only the promoted version of
|
||||
-- this type is used.
|
||||
data Pillar = Cap !Pillar | Base
|
||||
|
||||
class ToEmptyCornice (p :: Pillar) where
|
||||
toEmptyCornice :: Cornice p a c
|
||||
|
||||
instance ToEmptyCornice Base where
|
||||
toEmptyCornice = CorniceBase mempty
|
||||
|
||||
instance ToEmptyCornice (Cap p) where
|
||||
toEmptyCornice = CorniceCap Vector.empty
|
||||
|
||||
data Fascia (p :: Pillar) r where
|
||||
FasciaBase :: !r -> Fascia Base r
|
||||
FasciaCap :: !r -> Fascia p r -> Fascia (Cap p) r
|
||||
|
||||
data OneCornice k (p :: Pillar) a c = OneCornice
|
||||
{ oneCorniceHead :: !c
|
||||
, oneCorniceBody :: !(k p a c)
|
||||
}
|
||||
|
||||
data Cornice (p :: Pillar) a c where
|
||||
CorniceBase :: !(Colonnade Headed a c) -> Cornice Base a c
|
||||
CorniceCap :: {-# UNPACK #-} !(Vector (OneCornice Cornice p a c)) -> Cornice (Cap p) a c
|
||||
|
||||
instance Semigroup (Cornice p a c) where
|
||||
CorniceBase a <> CorniceBase b = CorniceBase (mappend a b)
|
||||
CorniceCap a <> CorniceCap b = CorniceCap (a Vector.++ b)
|
||||
sconcat xs@(x :| _) = case x of
|
||||
CorniceBase _ -> CorniceBase (Colonnade (vectorConcatNE (fmap (getColonnade . getCorniceBase) xs)))
|
||||
CorniceCap _ -> CorniceCap (vectorConcatNE (fmap getCorniceCap xs))
|
||||
|
||||
instance ToEmptyCornice p => Monoid (Cornice p a c) where
|
||||
mempty = toEmptyCornice
|
||||
mappend = (Semigroup.<>)
|
||||
mconcat xs1 = case xs1 of
|
||||
[] -> toEmptyCornice
|
||||
x : xs2 -> Semigroup.sconcat (x :| xs2)
|
||||
|
||||
getCorniceBase :: Cornice Base a c -> Colonnade Headed a c
|
||||
getCorniceBase (CorniceBase c) = c
|
||||
|
||||
getCorniceCap :: Cornice (Cap p) a c -> Vector (OneCornice Cornice p a c)
|
||||
getCorniceCap (CorniceCap c) = c
|
||||
|
||||
data AnnotatedCornice (p :: Pillar) a c where
|
||||
AnnotatedCorniceBase :: !(Maybe Int) -> !(Colonnade (Sized Headed) a c) -> AnnotatedCornice Base a c
|
||||
AnnotatedCorniceCap ::
|
||||
!(Maybe Int)
|
||||
-> {-# UNPACK #-} !(Vector (OneCornice AnnotatedCornice p a c))
|
||||
-> AnnotatedCornice (Cap p) a c
|
||||
|
||||
data MutableSizedCornice s (p :: Pillar) a c where
|
||||
MutableSizedCorniceBase ::
|
||||
{-# UNPACK #-} !(MutableSizedColonnade s Headed a c)
|
||||
-> MutableSizedCornice s Base a c
|
||||
MutableSizedCorniceCap ::
|
||||
{-# UNPACK #-} !(Vector (OneCornice (MutableSizedCornice s) p a c))
|
||||
-> MutableSizedCornice s (Cap p) a c
|
||||
|
||||
-- data MaybeInt = JustInt {-# UNPACK #-} !Int | NothingInt
|
||||
|
||||
-- | This is provided with vector-0.12, but we include a copy here
|
||||
-- for compatibility.
|
||||
vectorConcatNE :: NonEmpty (Vector a) -> Vector a
|
||||
vectorConcatNE = Vector.concat . toList
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE DeriveFoldable #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
module Colonnade.Types
|
||||
( Colonnade(..)
|
||||
, Decolonnade(..)
|
||||
, OneColonnade(..)
|
||||
, Headed(..)
|
||||
, Headless(..)
|
||||
, Indexed(..)
|
||||
, HeadingErrors(..)
|
||||
, DecolonnadeCellError(..)
|
||||
, DecolonnadeRowError(..)
|
||||
, DecolonnadeCellErrors(..)
|
||||
, RowError(..)
|
||||
) where
|
||||
|
||||
import Data.Vector (Vector)
|
||||
import Data.Functor.Contravariant (Contravariant(..))
|
||||
import Data.Functor.Contravariant.Divisible (Divisible(..))
|
||||
import Control.Exception (Exception)
|
||||
import Data.Typeable (Typeable)
|
||||
import qualified Data.Vector as Vector
|
||||
|
||||
-- | This type is isomorphic to 'Identity'.
|
||||
newtype Headed a = Headed { getHeaded :: a }
|
||||
deriving (Eq,Ord,Functor,Show,Read,Foldable)
|
||||
|
||||
-- | This type is isomorphic to 'Proxy'
|
||||
data Headless a = Headless
|
||||
deriving (Eq,Ord,Functor,Show,Read,Foldable)
|
||||
|
||||
data Indexed f a = Indexed
|
||||
{ indexedIndex :: !Int
|
||||
, indexedHeading :: !(f a)
|
||||
} deriving (Eq,Ord,Functor,Show,Read)
|
||||
|
||||
data HeadingErrors content = HeadingErrors
|
||||
{ headingErrorsMissing :: Vector content -- ^ headers that were missing
|
||||
, headingErrorsDuplicate :: Vector (content,Int) -- ^ headers that occurred more than once
|
||||
} deriving (Show,Read,Eq)
|
||||
|
||||
instance (Show content, Typeable content) => Exception (HeadingErrors content)
|
||||
|
||||
instance Monoid (HeadingErrors content) where
|
||||
mempty = HeadingErrors Vector.empty Vector.empty
|
||||
mappend (HeadingErrors a1 b1) (HeadingErrors a2 b2) = HeadingErrors
|
||||
(a1 Vector.++ a2) (b1 Vector.++ b2)
|
||||
|
||||
data DecolonnadeCellError f content = DecolonnadeCellError
|
||||
{ decodingCellErrorContent :: !content
|
||||
, decodingCellErrorHeader :: !(Indexed f content)
|
||||
, decodingCellErrorMessage :: !String
|
||||
} deriving (Show,Read,Eq)
|
||||
|
||||
-- instance (Show (f content), Typeable content) => Exception (DecolonnadeError f content)
|
||||
|
||||
newtype DecolonnadeCellErrors f content = DecolonnadeCellErrors
|
||||
{ getDecolonnadeCellErrors :: Vector (DecolonnadeCellError f content)
|
||||
} deriving (Monoid,Show,Read,Eq)
|
||||
|
||||
-- newtype ParseRowError = ParseRowError String
|
||||
|
||||
-- TODO: rewrite the instances for this by hand. They
|
||||
-- currently use FlexibleContexts.
|
||||
data DecolonnadeRowError f content = DecolonnadeRowError
|
||||
{ decodingRowErrorRow :: !Int
|
||||
, decodingRowErrorError :: !(RowError f content)
|
||||
} deriving (Show,Read,Eq)
|
||||
|
||||
-- TODO: rewrite the instances for this by hand. They
|
||||
-- currently use FlexibleContexts.
|
||||
data RowError f content
|
||||
= RowErrorParse !String -- ^ Error occurred parsing the document into cells
|
||||
| RowErrorDecode !(DecolonnadeCellErrors f content) -- ^ Error decoding the content
|
||||
| RowErrorSize !Int !Int -- ^ Wrong number of cells in the row
|
||||
| RowErrorHeading !(HeadingErrors content)
|
||||
| RowErrorMinSize !Int !Int
|
||||
| RowErrorMalformed !String -- ^ Error decoding unicode content
|
||||
deriving (Show,Read,Eq)
|
||||
|
||||
-- instance (Show (f content), Typeable content) => Exception (DecolonnadeErrors f content)
|
||||
|
||||
instance Contravariant Headless where
|
||||
contramap _ Headless = Headless
|
||||
|
||||
-- | This just actually a specialization of the free applicative.
|
||||
-- Check out @Control.Applicative.Free@ in the @free@ library to
|
||||
-- learn more about this. The meanings of the fields are documented
|
||||
-- slightly more in the source code. Unfortunately, haddock does not
|
||||
-- play nicely with GADTs.
|
||||
data Decolonnade f content a where
|
||||
DecolonnadePure :: !a -- function
|
||||
-> Decolonnade f content a
|
||||
DecolonnadeAp :: !(f content) -- header
|
||||
-> !(content -> Either String a) -- decoding function
|
||||
-> !(Decolonnade f content (a -> b)) -- next decoding
|
||||
-> Decolonnade f content b
|
||||
|
||||
instance Functor (Decolonnade f content) where
|
||||
fmap f (DecolonnadePure a) = DecolonnadePure (f a)
|
||||
fmap f (DecolonnadeAp h c apNext) = DecolonnadeAp h c ((f .) <$> apNext)
|
||||
|
||||
instance Applicative (Decolonnade f content) where
|
||||
pure = DecolonnadePure
|
||||
DecolonnadePure f <*> y = fmap f y
|
||||
DecolonnadeAp h c y <*> z = DecolonnadeAp h c (flip <$> y <*> z)
|
||||
|
||||
-- | Encodes a header and a cell.
|
||||
data OneColonnade f content a = OneColonnade
|
||||
{ oneColonnadeHead :: !(f content)
|
||||
, oneColonnadeEncode :: !(a -> content)
|
||||
}
|
||||
|
||||
instance Contravariant (OneColonnade f content) where
|
||||
contramap f (OneColonnade h e) = OneColonnade h (e . f)
|
||||
|
||||
-- | An columnar encoding of @a@. The type variable @f@ determines what
|
||||
-- is present in each column in the header row. It is typically instantiated
|
||||
-- to 'Headed' and occasionally to 'Headless'. There is nothing that
|
||||
-- restricts it to these two types, although they satisfy the majority
|
||||
-- of use cases. The type variable @c@ is the content type. This can
|
||||
-- be @Text@, @String@, or @ByteString@. In the companion libraries
|
||||
-- @reflex-dom-colonnade@ and @yesod-colonnade@, additional types
|
||||
-- that represent HTML with element attributes are provided that serve
|
||||
-- as the content type.
|
||||
--
|
||||
-- Internally, a 'Colonnade' is represented as a 'Vector' of individual
|
||||
-- column encodings. It is possible to use any collection type with
|
||||
-- 'Alternative' and 'Foldable' instances. However, 'Vector' was chosen to
|
||||
-- optimize the data structure for the use case of building the structure
|
||||
-- once and then folding over it many times. It is recommended that
|
||||
-- 'Colonnade's are defined at the top-level so that GHC avoid reconstructing
|
||||
-- them every time they are used.
|
||||
newtype Colonnade f c a = Colonnade
|
||||
{ getColonnade :: Vector (OneColonnade f c a)
|
||||
} deriving (Monoid)
|
||||
|
||||
instance Contravariant (Colonnade f content) where
|
||||
contramap f (Colonnade v) = Colonnade
|
||||
(Vector.map (contramap f) v)
|
||||
|
||||
instance Divisible (Colonnade f content) where
|
||||
conquer = Colonnade Vector.empty
|
||||
divide f (Colonnade a) (Colonnade b) =
|
||||
Colonnade $ (Vector.++)
|
||||
(Vector.map (contramap (fst . f)) a)
|
||||
(Vector.map (contramap (snd . f)) b)
|
||||
-- (Vector.map (\(OneEncoding h c) -> (h,c . fst . f)) a)
|
||||
-- (Vector.map (\(OneEncoding h c) -> (h,c . snd . f)) b)
|
||||
|
||||
@ -2,5 +2,5 @@ import Test.DocTest
|
||||
|
||||
main :: IO ()
|
||||
main = doctest
|
||||
[ "src/Colonnade.hs"
|
||||
[ "src"
|
||||
]
|
||||
|
||||
30
lucid-colonnade/LICENSE
Normal file
30
lucid-colonnade/LICENSE
Normal file
@ -0,0 +1,30 @@
|
||||
Copyright Andrew Martin (c) 2016
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Andrew Martin nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
2
lucid-colonnade/Setup.hs
Normal file
2
lucid-colonnade/Setup.hs
Normal file
@ -0,0 +1,2 @@
|
||||
import Distribution.Simple
|
||||
main = defaultMain
|
||||
29
lucid-colonnade/lucid-colonnade.cabal
Normal file
29
lucid-colonnade/lucid-colonnade.cabal
Normal file
@ -0,0 +1,29 @@
|
||||
name: lucid-colonnade
|
||||
version: 1.0.1
|
||||
synopsis: Helper functions for using lucid with colonnade
|
||||
description: Lucid and colonnade
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2017 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules:
|
||||
Lucid.Colonnade
|
||||
build-depends:
|
||||
base >= 4.8 && < 5
|
||||
, colonnade >= 1.1.1 && < 1.3
|
||||
, lucid >= 2.9 && < 3.0
|
||||
, text >= 1.2 && < 1.3
|
||||
, vector >= 0.10 && < 0.13
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/andrewthad/colonnade
|
||||
292
lucid-colonnade/src/Lucid/Colonnade.hs
Normal file
292
lucid-colonnade/src/Lucid/Colonnade.hs
Normal file
@ -0,0 +1,292 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | Build HTML tables using @lucid@ and @colonnade@. It is
|
||||
-- recommended that users read the documentation for @colonnade@ first,
|
||||
-- since this library builds on the abstractions introduced there.
|
||||
-- Also, look at the docs for @blaze-colonnade@. These two
|
||||
-- libraries are similar, but blaze offers an HTML pretty printer
|
||||
-- which makes it possible to doctest examples. Since lucid
|
||||
-- does not offer such facilities, examples are omitted here.
|
||||
module Lucid.Colonnade
|
||||
( -- * Apply
|
||||
encodeHtmlTable
|
||||
, encodeCellTable
|
||||
, encodeCellTableSized
|
||||
, encodeTable
|
||||
-- * Cell
|
||||
-- $build
|
||||
, Cell(..)
|
||||
, htmlCell
|
||||
, stringCell
|
||||
, textCell
|
||||
, lazyTextCell
|
||||
, builderCell
|
||||
, htmlFromCell
|
||||
, encodeBodySized
|
||||
, sectioned
|
||||
-- * Discussion
|
||||
-- $discussion
|
||||
) where
|
||||
|
||||
import Colonnade (Colonnade,Headed,Headless,Fascia,Cornice)
|
||||
import Data.Text (Text)
|
||||
import Control.Monad
|
||||
import Data.Semigroup
|
||||
import Data.Monoid hiding ((<>))
|
||||
import Data.Foldable
|
||||
import Data.String (IsString(..))
|
||||
import Data.Maybe (listToMaybe)
|
||||
import Data.Char (isSpace)
|
||||
import Control.Applicative (liftA2)
|
||||
import Lucid hiding (for_)
|
||||
import qualified Colonnade as Col
|
||||
import qualified Data.List as List
|
||||
import qualified Colonnade.Encode as E
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Text.Lazy as LText
|
||||
import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.Text as T
|
||||
|
||||
-- $build
|
||||
--
|
||||
-- The 'Cell' type is used to build a 'Colonnade' that
|
||||
-- has 'Html' content inside table cells and may optionally
|
||||
-- have attributes added to the @\<td\>@ or @\<th\>@ elements
|
||||
-- that wrap this HTML content.
|
||||
|
||||
-- | The attributes that will be applied to a @\<td\>@ and
|
||||
-- the HTML content that will go inside it. When using
|
||||
-- this type, remember that 'Attribute', defined in @blaze-markup@,
|
||||
-- is actually a collection of attributes, not a single attribute.
|
||||
data Cell d = Cell
|
||||
{ cellAttribute :: ![Attribute]
|
||||
, cellHtml :: !(Html d)
|
||||
}
|
||||
|
||||
instance (d ~ ()) => IsString (Cell d) where
|
||||
fromString = stringCell
|
||||
|
||||
instance Semigroup d => Semigroup (Cell d) where
|
||||
Cell a1 c1 <> Cell a2 c2 = Cell (mappend a1 a2) (liftA2 (<>) c1 c2)
|
||||
|
||||
instance Monoid d => Monoid (Cell d) where
|
||||
mempty = Cell mempty (return mempty)
|
||||
mappend (Cell a1 c1) (Cell a2 c2) = Cell (mappend a1 a2) (liftA2 mappend c1 c2)
|
||||
|
||||
-- | Create a 'Cell' from a 'Widget'
|
||||
htmlCell :: Html d -> Cell d
|
||||
htmlCell = Cell mempty
|
||||
|
||||
-- | Create a 'Cell' from a 'String'
|
||||
stringCell :: String -> Cell ()
|
||||
stringCell = htmlCell . fromString
|
||||
|
||||
-- | Create a 'Cell' from a 'Char'
|
||||
charCell :: Char -> Cell ()
|
||||
charCell = stringCell . pure
|
||||
|
||||
-- | Create a 'Cell' from a 'Text'
|
||||
textCell :: Text -> Cell ()
|
||||
textCell = htmlCell . toHtml
|
||||
|
||||
-- | Create a 'Cell' from a lazy text
|
||||
lazyTextCell :: LText.Text -> Cell ()
|
||||
lazyTextCell = textCell . LText.toStrict
|
||||
|
||||
-- | Create a 'Cell' from a text builder
|
||||
builderCell :: TBuilder.Builder -> Cell ()
|
||||
builderCell = lazyTextCell . TBuilder.toLazyText
|
||||
|
||||
-- | Encode a table. Table cell element do not have
|
||||
-- any attributes applied to them.
|
||||
encodeHtmlTable ::
|
||||
(E.Headedness h, Foldable f, Monoid d)
|
||||
=> [Attribute] -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade h a (Html d) -- ^ How to encode data as columns
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html d
|
||||
encodeHtmlTable = encodeTable
|
||||
(E.headednessPure ([],[])) mempty (const mempty) (\el -> el [])
|
||||
|
||||
-- | Encode a table. Table cells may have attributes applied
|
||||
-- to them
|
||||
encodeCellTable ::
|
||||
(E.Headedness h, Foldable f, Monoid d)
|
||||
=> [Attribute] -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade h a (Cell d) -- ^ How to encode data as columns
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html d
|
||||
encodeCellTable = encodeTable
|
||||
(E.headednessPure ([],[])) mempty (const mempty) htmlFromCell
|
||||
|
||||
encodeCellTableSized ::
|
||||
(E.Headedness h, Foldable f, Monoid d)
|
||||
=> [Attribute] -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade (E.Sized Int h) a (Cell d) -- ^ How to encode data as columns
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html ()
|
||||
encodeCellTableSized = encodeTableSized
|
||||
(E.headednessPure ([],[])) mempty (const mempty) htmlFromCell
|
||||
|
||||
-- | Encode a table. This handles a very general case and
|
||||
-- is seldom needed by users. One of the arguments provided is
|
||||
-- used to add attributes to the generated @\<tr\>@ elements.
|
||||
-- The elements of type @d@ produced by generating html are
|
||||
-- strictly combined with their monoidal append function.
|
||||
-- However, this type is nearly always @()@.
|
||||
encodeTable :: forall f h a d c.
|
||||
(Foldable f, E.Headedness h, Monoid d)
|
||||
=> h ([Attribute],[Attribute]) -- ^ Attributes of @\<thead\>@ and its @\<tr\>@
|
||||
-> [Attribute] -- ^ Attributes of @\<tbody\>@ element
|
||||
-> (a -> [Attribute]) -- ^ Attributes of each @\<tr\>@ element
|
||||
-> (([Attribute] -> Html d -> Html d) -> c -> Html d) -- ^ Wrap content and convert to 'Html'
|
||||
-> [Attribute] -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade h a c -- ^ How to encode data as a row
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html d
|
||||
encodeTable mtheadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
|
||||
table_ tableAttrs $ do
|
||||
d1 <- case E.headednessExtractForall of
|
||||
Nothing -> return mempty
|
||||
Just extractForall -> do
|
||||
let (theadAttrs,theadTrAttrs) = extract mtheadAttrs
|
||||
thead_ theadAttrs $ tr_ theadTrAttrs $ do
|
||||
foldlMapM' (wrapContent th_ . extract . E.oneColonnadeHead) (E.getColonnade colonnade)
|
||||
where
|
||||
extract :: forall y. h y -> y
|
||||
extract = E.runExtractForall extractForall
|
||||
d2 <- encodeBody trAttrs wrapContent tbodyAttrs colonnade xs
|
||||
return (mappend d1 d2)
|
||||
|
||||
encodeBody :: (Foldable f, Monoid d)
|
||||
=> (a -> [Attribute]) -- ^ Attributes of each @\<tr\>@ element
|
||||
-> (([Attribute] -> Html d -> Html d) -> c -> Html d) -- ^ Wrap content and convert to 'Html'
|
||||
-> [Attribute] -- ^ Attributes of @\<tbody\>@ element
|
||||
-> Colonnade h a c -- ^ How to encode data as a row
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html d
|
||||
encodeBody trAttrs wrapContent tbodyAttrs colonnade xs = do
|
||||
tbody_ tbodyAttrs $ do
|
||||
flip foldlMapM' xs $ \x -> do
|
||||
tr_ (trAttrs x) $ E.rowMonadic colonnade (wrapContent td_) x
|
||||
|
||||
encodeBodySized ::
|
||||
(Foldable f, Monoid d)
|
||||
=> (a -> [Attribute])
|
||||
-> [Attribute]
|
||||
-> Colonnade (E.Sized Int h) a (Cell d)
|
||||
-> f a
|
||||
-> Html ()
|
||||
encodeBodySized trAttrs tbodyAttrs colonnade collection = tbody_ tbodyAttrs $ do
|
||||
for_ collection $ \a -> tr_ (trAttrs a) $ do
|
||||
E.rowMonoidalHeader
|
||||
colonnade
|
||||
(\(E.Sized sz _) (Cell cattr content) ->
|
||||
void $ td_ (setColspanOrHide sz cattr) content
|
||||
)
|
||||
a
|
||||
|
||||
encodeTableSized :: forall f h a d c.
|
||||
(Foldable f, E.Headedness h, Monoid d)
|
||||
=> h ([Attribute],[Attribute]) -- ^ Attributes of @\<thead\>@ and its @\<tr\>@
|
||||
-> [Attribute] -- ^ Attributes of @\<tbody\>@ element
|
||||
-> (a -> [Attribute]) -- ^ Attributes of each @\<tr\>@ element
|
||||
-> (([Attribute] -> Html d -> Html d) -> (Cell d) -> Html d) -- ^ Wrap content and convert to 'Html'
|
||||
-> [Attribute] -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade (E.Sized Int h) a (Cell d) -- ^ How to encode data as a row
|
||||
-> f a -- ^ Collection of data
|
||||
-> Html ()
|
||||
encodeTableSized mtheadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
|
||||
table_ tableAttrs $ do
|
||||
d1 <- case E.headednessExtractForall of
|
||||
Nothing -> pure mempty
|
||||
Just extractForall -> do
|
||||
let (theadAttrs,theadTrAttrs) = extract mtheadAttrs
|
||||
thead_ theadAttrs $ tr_ theadTrAttrs $ do
|
||||
traverse_
|
||||
(wrapContent th_ . extract .
|
||||
(\(E.Sized i h) -> case E.headednessExtract of
|
||||
Just f ->
|
||||
let (Cell attrs content) = f h
|
||||
in E.headednessPure $ Cell (setColspanOrHide i attrs) content
|
||||
Nothing -> E.headednessPure mempty
|
||||
-- (E.Headed (Cell attrs content)) -> E.Headed $ Cell (setColspanOrHide i attrs) content
|
||||
-- E.Headless -> E.Headless
|
||||
)
|
||||
. E.oneColonnadeHead
|
||||
)
|
||||
(E.getColonnade colonnade)
|
||||
where
|
||||
extract :: forall y. h y -> y
|
||||
extract = E.runExtractForall extractForall
|
||||
encodeBodySized trAttrs tbodyAttrs colonnade xs
|
||||
|
||||
setColspanOrHide :: Int -> [Attribute] -> [Attribute]
|
||||
setColspanOrHide i attrs
|
||||
| i < 1 = style_ "display:none;" : attrs
|
||||
| otherwise = colspan_ (Text.pack (show i)) : attrs
|
||||
|
||||
foldlMapM' :: forall g b a m. (Foldable g, Monoid b, Monad m) => (a -> m b) -> g a -> m b
|
||||
foldlMapM' f xs = foldr f' pure xs mempty
|
||||
where
|
||||
f' :: a -> (b -> m b) -> b -> m b
|
||||
f' x k bl = do
|
||||
br <- f x
|
||||
let !b = mappend bl br
|
||||
k b
|
||||
|
||||
-- | Convert a 'Cell' to 'Html' by wrapping the content with a tag
|
||||
-- and applying the 'Cell' attributes to that tag.
|
||||
htmlFromCell :: ([Attribute] -> Html d -> Html d) -> Cell d -> Html d
|
||||
htmlFromCell f (Cell attr content) = f attr content
|
||||
|
||||
-- $discussion
|
||||
--
|
||||
-- In this module, some of the functions for applying a 'Colonnade' to
|
||||
-- some values to build a table have roughly this type signature:
|
||||
--
|
||||
-- > Foldable a => Colonnade Headedness a (Cell d) -> f a -> Html d
|
||||
--
|
||||
-- The 'Colonnade' content type is 'Cell', but the content
|
||||
-- type of the result is 'Html'. It may not be immidiately clear why
|
||||
-- this is done. Another strategy, which this library also
|
||||
-- uses, is to write
|
||||
-- these functions to take a 'Colonnade' whose content is 'Html':
|
||||
--
|
||||
-- > Foldable a => Colonnade Headedness a (Html d) -> f a -> Html d
|
||||
--
|
||||
-- When the 'Colonnade' content type is 'Html', then the header
|
||||
-- content is rendered as the child of a @\<th\>@ and the row
|
||||
-- content the child of a @\<td\>@. However, it is not possible
|
||||
-- to add attributes to these parent elements. To accomodate this
|
||||
-- situation, it is necessary to introduce 'Cell', which includes
|
||||
-- the possibility of attributes on the parent node.
|
||||
|
||||
sectioned ::
|
||||
(Foldable f, E.Headedness h, Foldable g, Monoid c)
|
||||
=> [Attribute] -- ^ @\<table\>@ tag attributes
|
||||
-> Maybe ([Attribute], [Attribute])
|
||||
-- ^ Attributes of @\<thead\>@ and its @\<tr\>@, pass 'Nothing' to omit @\<thead\>@
|
||||
-> [Attribute] -- ^ @\<tbody\>@ tag attributes
|
||||
-> (a -> [Attribute]) -- ^ @\<tr\>@ tag attributes for data rows
|
||||
-> (b -> Cell c) -- ^ Section divider encoding strategy
|
||||
-> Colonnade h a (Cell c) -- ^ Data encoding strategy
|
||||
-> f (b, g a) -- ^ Collection of data
|
||||
-> Html ()
|
||||
sectioned tableAttrs mheadAttrs bodyAttrs trAttrs dividerContent colonnade@(E.Colonnade v) collection = do
|
||||
let vlen = V.length v
|
||||
table_ tableAttrs $ do
|
||||
for_ mheadAttrs $ \(headAttrs,headTrAttrs) ->
|
||||
thead_ headAttrs . tr_ headTrAttrs $
|
||||
E.headerMonadicGeneral_ colonnade (htmlFromCell th_)
|
||||
tbody_ bodyAttrs $ for_ collection $ \(b,as) -> do
|
||||
let Cell attrs contents = dividerContent b
|
||||
tr_ [] $ do
|
||||
td_ ((colspan_ $ T.pack (show vlen)): attrs) contents
|
||||
flip traverse_ as $ \a -> do
|
||||
tr_ (trAttrs a) $ E.rowMonadic colonnade (htmlFromCell td_) a
|
||||
|
||||
73
nix/default.nix
Normal file
73
nix/default.nix
Normal file
@ -0,0 +1,73 @@
|
||||
{ frontend ? false }:
|
||||
|
||||
let _nixpkgs = import <nixpkgs> {};
|
||||
nixpkgs = _nixpkgs.fetchFromGitHub {
|
||||
owner = "NixOS";
|
||||
repo = "nixpkgs";
|
||||
rev = "5c4a404b0d0e5125070dde5c1787210149157e83";
|
||||
sha256 = "0a478l0dxzy5hglavkilxjkh45zfg31q50hgkv1npninc4lpv5f7";
|
||||
};
|
||||
pkgs = import nixpkgs { config = {}; overlays = []; };
|
||||
|
||||
fetch-github-json = owner: repo: path:
|
||||
let commit = builtins.fromJSON (builtins.readFile path);
|
||||
in pkgs.fetchFromGitHub {
|
||||
name = "${repo}-${commit.rev}";
|
||||
inherit owner repo;
|
||||
inherit (commit) rev sha256;
|
||||
};
|
||||
|
||||
reflex-platform = import (fetch-github-json "layer-3-communications" "reflex-platform" ./reflex-platform.json) {};
|
||||
jsaddle-src = fetch-github-json "ghcjs" "jsaddle" ./jsaddle.json;
|
||||
compiler = "ghc8_2_1";
|
||||
|
||||
filterPredicate = p: type:
|
||||
let path = baseNameOf p; in !(
|
||||
(type == "directory" && pkgs.lib.hasPrefix "dist" path)
|
||||
|| (type == "symlink" && pkgs.lib.hasPrefix "result" path)
|
||||
|| pkgs.lib.hasPrefix ".ghc" path
|
||||
|| pkgs.lib.hasPrefix ".git" path
|
||||
|| pkgs.lib.hasSuffix "~" path
|
||||
|| pkgs.lib.hasSuffix ".o" path
|
||||
|| pkgs.lib.hasSuffix ".so" path
|
||||
|| pkgs.lib.hasSuffix ".nix" path);
|
||||
|
||||
overrides = reflex-platform.${compiler}.override {
|
||||
overrides = self: super:
|
||||
with reflex-platform;
|
||||
with reflex-platform.lib;
|
||||
with reflex-platform.nixpkgs.haskell.lib;
|
||||
with reflex-platform.nixpkgs.haskellPackages;
|
||||
let
|
||||
cp = file: (self.callPackage (./deps + "/${file}.nix") {});
|
||||
build-from-json = name: str: self.callCabal2nix name str {};
|
||||
build = name: path: self.callCabal2nix name (builtins.filterSource filterPredicate path) {};
|
||||
in
|
||||
{
|
||||
gtk2hs-buildtools = self.callPackage ./gtk2hs-buildtools.nix {};
|
||||
colonnade = build "colonnade" ../colonnade;
|
||||
siphon = build "siphon" ../siphon;
|
||||
reflex-dom-colonnade = build "reflex-dom-colonnade" ../reflex-dom-colonnade;
|
||||
lucid-colonnade = build "lucid-colonnade" ../lucid-colonnade;
|
||||
blaze-colonnade = build "blaze-colonnade" ../blaze-colonnade;
|
||||
yesod-colonnade = build "yesod-colonnade" ../yesod-colonnade;
|
||||
} //
|
||||
{
|
||||
jsaddle = doJailbreak (build-from-json "jsaddle" "${jsaddle-src}/jsaddle");
|
||||
jsaddle-webkitgtk = doJailbreak (build-from-json "jsaddle-webkitgtk" "${jsaddle-src}/jsaddle-webkitgtk");
|
||||
jsaddle-webkit2gtk = doJailbreak (build-from-json "jsaddle-webkit2gtk" "${jsaddle-src}/jsaddle-webkit2gtk");
|
||||
jsaddle-wkwebview = doJailbreak (build-from-json "jsaddle-wkwebview" "${jsaddle-src}/jsaddle-wkwebview");
|
||||
jsaddle-clib = doJailbreak (build-from-json "jsaddle-clib" "${jsaddle-src}/jsaddle-clib");
|
||||
jsaddle-warp = dontCheck (doJailbreak (build-from-json "jsaddle-warp" "${jsaddle-src}/jsaddle-warp"));
|
||||
};
|
||||
|
||||
};
|
||||
in rec {
|
||||
inherit reflex-platform fetch-github-json overrides nixpkgs pkgs;
|
||||
colonnade = overrides.colonnade;
|
||||
siphon = overrides.siphon;
|
||||
reflex-dom-colonnade = overrides.reflex-dom-colonnade;
|
||||
lucid-colonnade = overrides.lucid-colonnade;
|
||||
blaze-colonnade = overrides.blaze-colonnade;
|
||||
yesod-colonnade = overrides.yesod-colonnade;
|
||||
}
|
||||
20
nix/gtk2hs-buildtools.nix
Normal file
20
nix/gtk2hs-buildtools.nix
Normal file
@ -0,0 +1,20 @@
|
||||
{ mkDerivation, alex, array, base, Cabal, containers, directory
|
||||
, filepath, happy, hashtables, pretty, process, random, stdenv
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "gtk2hs-buildtools";
|
||||
version = "0.13.4.0";
|
||||
sha256 = "0f3e6ba90839efd43efe8cecbddb6478a55e2ce7788c57a0add4df477dede679";
|
||||
isLibrary = true;
|
||||
isExecutable = true;
|
||||
enableSeparateDataOutput = true;
|
||||
libraryHaskellDepends = [
|
||||
array base Cabal containers directory filepath hashtables pretty
|
||||
process random
|
||||
];
|
||||
libraryToolDepends = [ alex happy ];
|
||||
executableHaskellDepends = [ base ];
|
||||
homepage = "http://projects.haskell.org/gtk2hs/";
|
||||
description = "Tools to build the Gtk2Hs suite of User Interface libraries";
|
||||
license = stdenv.lib.licenses.gpl2;
|
||||
}
|
||||
6
nix/jsaddle.json
Normal file
6
nix/jsaddle.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"owner": "ghcjs",
|
||||
"repo": "jsaddle",
|
||||
"rev": "b423436565fce7f69a65d843c71fc52dc455bf54",
|
||||
"sha256": "09plndkh5wnbqi34x3jpaz0kjdjgyf074faf5xk97rsm81vhz8kk"
|
||||
}
|
||||
7
nix/overrides.nix
Normal file
7
nix/overrides.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{ options ? (x: x), filterPredicate ? (x: true), lib, cabal2nixResult, self, super }:
|
||||
let build = path: options (self.callPackage (cabal2nixResult (builtins.filterSource filterPredicate path)) {});
|
||||
in {
|
||||
# Core Libraries
|
||||
colonnade = lib.dontCheck (build ../colonnade);
|
||||
reflex-dom-colonnade = build ../reflex-dom-colonnade;
|
||||
}
|
||||
7
nix/reflex-platform.json
Normal file
7
nix/reflex-platform.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"url": "https://github.com/reflex-frp/reflex-platform",
|
||||
"rev": "0446e9df3adfc7271015c278a2ec5b7e7a6a46f3",
|
||||
"date": "2017-05-05T11:40:26-04:00",
|
||||
"sha256": "0v0d53xqrmh0i01iiq1flq66gw3cb6g9894j94cflsavmhih8y1d",
|
||||
"fetchSubmodules": true
|
||||
}
|
||||
4
projects/cabal-8.0.2.project
Normal file
4
projects/cabal-8.0.2.project
Normal file
@ -0,0 +1,4 @@
|
||||
packages: ./colonnade
|
||||
./blaze-colonnade
|
||||
./lucid-colonnade
|
||||
./yesod-colonnade
|
||||
4
projects/cabal-8.2.2.project
Normal file
4
projects/cabal-8.2.2.project
Normal file
@ -0,0 +1,4 @@
|
||||
packages: ./colonnade
|
||||
./blaze-colonnade
|
||||
./lucid-colonnade
|
||||
./yesod-colonnade
|
||||
3
projects/cabal-8.4.3.project
Normal file
3
projects/cabal-8.4.3.project
Normal file
@ -0,0 +1,3 @@
|
||||
packages: ./colonnade
|
||||
./blaze-colonnade
|
||||
./lucid-colonnade
|
||||
8
reflex-dom-colonnade/default.nix
Normal file
8
reflex-dom-colonnade/default.nix
Normal file
@ -0,0 +1,8 @@
|
||||
{ frontend ? false }:
|
||||
let
|
||||
pname = "reflex-dom-colonnade";
|
||||
main = (import ../nix/default.nix {
|
||||
inherit frontend;
|
||||
});
|
||||
in
|
||||
main.${pname}
|
||||
7
reflex-dom-colonnade/overrides-ghc.nix
Normal file
7
reflex-dom-colonnade/overrides-ghc.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{ reflex-platform, ... }:
|
||||
let dc = reflex-platform.nixpkgs.haskell.lib.dontCheck;
|
||||
in reflex-platform.ghc.override {
|
||||
overrides = self: super: {
|
||||
colonnade = dc (self.callPackage (reflex-platform.cabal2nixResult ../colonnade) {});
|
||||
};
|
||||
}
|
||||
@ -1,33 +1,33 @@
|
||||
name: reflex-dom-colonnade
|
||||
version: 0.4.6
|
||||
synopsis: Use colonnade with reflex-dom
|
||||
description: Please see README.md
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2016 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
name: reflex-dom-colonnade
|
||||
version: 0.6.0
|
||||
synopsis: Use colonnade with reflex-dom
|
||||
description: Please see README.md
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2016 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules:
|
||||
Reflex.Dom.Colonnade
|
||||
build-depends:
|
||||
base >= 4.7 && < 5.0
|
||||
, colonnade >= 0.4.6 && < 0.5
|
||||
base >= 4.9 && < 5.0
|
||||
, colonnade >= 1.2 && < 1.3
|
||||
, contravariant >= 1.2 && < 1.5
|
||||
, vector >= 0.10 && < 0.12
|
||||
, vector >= 0.10 && < 0.13
|
||||
, text >= 1.0 && < 1.3
|
||||
, reflex
|
||||
, reflex-dom
|
||||
, reflex == 0.5.*
|
||||
, reflex-dom == 0.4.*
|
||||
, containers >= 0.5 && < 0.6
|
||||
, semigroups >= 0.16 && < 0.19
|
||||
, profunctors >= 5.2 && < 5.3
|
||||
, transformers >= 0.5 && < 0.6
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
|
||||
1
reflex-dom-colonnade/shell.nix
Normal file
1
reflex-dom-colonnade/shell.nix
Normal file
@ -0,0 +1 @@
|
||||
(import ./. {}).env
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,44 +1,48 @@
|
||||
name: siphon
|
||||
version: 0.2
|
||||
synopsis: Generic types and functions for columnar encoding and decoding
|
||||
description: Please see README.md
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2016 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
name: siphon
|
||||
version: 0.8.1.1
|
||||
synopsis: Encode and decode CSV files
|
||||
description: Please see README.md
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2016 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
hs-source-dirs: src
|
||||
exposed-modules:
|
||||
Siphon.Text
|
||||
Siphon.ByteString.Char8
|
||||
Siphon
|
||||
Siphon.Types
|
||||
Siphon.Content
|
||||
Siphon.Encoding
|
||||
Siphon.Decoding
|
||||
Siphon.Internal
|
||||
Siphon.Internal.Text
|
||||
build-depends:
|
||||
base >= 4.7 && < 5
|
||||
, colonnade >= 0.4 && < 0.5
|
||||
, text
|
||||
base >= 4.8 && < 5
|
||||
, colonnade >= 1.2 && < 1.3
|
||||
, text >= 1.0 && < 1.3
|
||||
, bytestring
|
||||
, contravariant
|
||||
, vector
|
||||
, pipes
|
||||
, streaming >= 0.1.4 && < 0.3
|
||||
, attoparsec
|
||||
default-language: Haskell2010
|
||||
, transformers >= 0.4.2 && < 0.6
|
||||
, semigroups >= 0.18.2 && < 0.20
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite siphon-test
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Test.hs
|
||||
test-suite doctest
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Doctest.hs
|
||||
build-depends:
|
||||
base
|
||||
, siphon
|
||||
, doctest >= 0.10
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite test
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Test.hs
|
||||
build-depends:
|
||||
base
|
||||
, either
|
||||
@ -53,9 +57,10 @@ test-suite siphon-test
|
||||
, pipes
|
||||
, HUnit
|
||||
, test-framework-hunit
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
default-language: Haskell2010
|
||||
, profunctors
|
||||
, streaming
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
type: git
|
||||
location: https://github.com/andrewthad/colonnade
|
||||
|
||||
@ -1,11 +1,769 @@
|
||||
module Siphon where
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
|
||||
-- encode :: Pipe a (Vector c) m x
|
||||
-- encode
|
||||
-- decode :: Pipe (Vector c) a m x
|
||||
{-# OPTIONS_GHC -Wall -fno-warn-unused-imports #-}
|
||||
|
||||
-- encode ::
|
||||
-- | Build CSVs using the abstractions provided in the @colonnade@ library, and
|
||||
-- parse CSVs using 'Siphon', which is the dual of 'Colonnade'.
|
||||
-- Read the documentation for @colonnade@ before reading the documentation
|
||||
-- for @siphon@. All of the examples on this page assume a common set of
|
||||
-- imports that are provided at the bottom of this page.
|
||||
module Siphon
|
||||
( -- * Encode CSV
|
||||
encodeCsv
|
||||
, encodeCsvStream
|
||||
, encodeCsvUtf8
|
||||
, encodeCsvStreamUtf8
|
||||
-- * Decode CSV
|
||||
, decodeCsvUtf8
|
||||
-- * Build Siphon
|
||||
, headed
|
||||
, headless
|
||||
, indexed
|
||||
-- * Types
|
||||
, Siphon
|
||||
, SiphonError(..)
|
||||
, Indexed(..)
|
||||
-- * Utility
|
||||
, humanizeSiphonError
|
||||
-- * Imports
|
||||
-- $setup
|
||||
) where
|
||||
|
||||
-- row :: Vector (Escaped Text) -> Text
|
||||
-- row = Vector.
|
||||
import Siphon.Types
|
||||
import Data.Monoid
|
||||
import Control.Applicative
|
||||
import Control.Monad
|
||||
|
||||
import qualified Data.ByteString.Char8 as BC8
|
||||
import qualified Data.Attoparsec.ByteString as A
|
||||
import qualified Data.Attoparsec.Lazy as AL
|
||||
import qualified Data.Attoparsec.Zepto as Z
|
||||
import qualified Data.ByteString as S
|
||||
import qualified Data.ByteString.Unsafe as S
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as LByteString
|
||||
import qualified Data.ByteString.Builder as Builder
|
||||
import qualified Data.Text.Lazy as LT
|
||||
import qualified Data.Text.Lazy.Builder as TB
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.List as L
|
||||
import qualified Streaming as SM
|
||||
import qualified Streaming.Prelude as SMP
|
||||
import qualified Data.Attoparsec.Types as ATYP
|
||||
import qualified Colonnade.Encode as CE
|
||||
import qualified Data.Vector.Mutable as MV
|
||||
import qualified Data.ByteString.Builder as BB
|
||||
import qualified Data.Semigroup as SG
|
||||
|
||||
import Control.Monad.Trans.Class
|
||||
import Data.Functor.Identity (Identity(..))
|
||||
import Data.ByteString.Builder (toLazyByteString,byteString)
|
||||
import Data.Attoparsec.ByteString.Char8 (char, endOfInput, string)
|
||||
import Data.Word (Word8)
|
||||
import Data.Vector (Vector)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Coerce (coerce)
|
||||
import Data.Char (chr)
|
||||
import Data.Text.Encoding (decodeUtf8')
|
||||
import Streaming (Stream,Of(..))
|
||||
import Data.Vector.Mutable (MVector)
|
||||
import Control.Monad.ST
|
||||
import Data.Text (Text)
|
||||
import Data.Semigroup (Semigroup)
|
||||
|
||||
newtype Escaped c = Escaped { getEscaped :: c }
|
||||
data Ended = EndedYes | EndedNo
|
||||
deriving (Show)
|
||||
data CellResult c = CellResultData !c | CellResultNewline !c !Ended
|
||||
deriving (Show)
|
||||
|
||||
decodeCsvUtf8 :: Monad m
|
||||
=> Siphon CE.Headed ByteString a
|
||||
-> Stream (Of ByteString) m () -- ^ encoded csv
|
||||
-> Stream (Of a) m (Maybe SiphonError)
|
||||
decodeCsvUtf8 headedSiphon s1 = do
|
||||
e <- lift (consumeHeaderRowUtf8 s1)
|
||||
case e of
|
||||
Left err -> return (Just err)
|
||||
Right (v :> s2) -> case headedToIndexed utf8ToStr v headedSiphon of
|
||||
Left err -> return (Just err)
|
||||
Right ixedSiphon -> do
|
||||
let requiredLength = V.length v
|
||||
consumeBodyUtf8 1 requiredLength ixedSiphon s2
|
||||
|
||||
encodeCsvStreamUtf8 :: (Monad m, CE.Headedness h)
|
||||
=> CE.Colonnade h a ByteString
|
||||
-> Stream (Of a) m r
|
||||
-> Stream (Of ByteString) m r
|
||||
encodeCsvStreamUtf8 =
|
||||
encodeCsvInternal escapeChar8 (B.singleton comma) (B.singleton newline)
|
||||
|
||||
-- | Streaming variant of 'encodeCsv'. This is particularly useful
|
||||
-- when you need to produce millions of rows without having them
|
||||
-- all loaded into memory at the same time.
|
||||
encodeCsvStream :: (Monad m, CE.Headedness h)
|
||||
=> CE.Colonnade h a Text
|
||||
-> Stream (Of a) m r
|
||||
-> Stream (Of Text) m r
|
||||
encodeCsvStream =
|
||||
encodeCsvInternal textEscapeChar8 (T.singleton ',') (T.singleton '\n')
|
||||
|
||||
-- | Encode a collection to a CSV as a text 'TB.Builder'. For example,
|
||||
-- we can take the following columnar encoding of a person:
|
||||
--
|
||||
-- >>> :{
|
||||
-- let colPerson :: Colonnade Headed Person Text
|
||||
-- colPerson = mconcat
|
||||
-- [ C.headed "Name" name
|
||||
-- , C.headed "Age" (T.pack . show . age)
|
||||
-- , C.headed "Company" (fromMaybe "N/A" . company)
|
||||
-- ]
|
||||
-- :}
|
||||
--
|
||||
-- And we have the following people whom we wish to encode
|
||||
-- in this way:
|
||||
--
|
||||
-- >>> :{
|
||||
-- let people :: [Person]
|
||||
-- people =
|
||||
-- [ Person "Chao" 26 (Just "Tectonic, Inc.")
|
||||
-- , Person "Elsie" 41 (Just "Globex Corporation")
|
||||
-- , Person "Arabella" 19 Nothing
|
||||
-- ]
|
||||
-- :}
|
||||
--
|
||||
-- We pair the encoding with the rows to get a CSV:
|
||||
--
|
||||
-- >>> LTIO.putStr (TB.toLazyText (encodeCsv colPerson people))
|
||||
-- Name,Age,Company
|
||||
-- Chao,26,"Tectonic, Inc."
|
||||
-- Elsie,41,Globex Corporation
|
||||
-- Arabella,19,N/A
|
||||
encodeCsv :: (Foldable f, CE.Headedness h)
|
||||
=> CE.Colonnade h a Text -- ^ Tablular encoding
|
||||
-> f a -- ^ Value of each row
|
||||
-> TB.Builder
|
||||
encodeCsv enc =
|
||||
textStreamToBuilder . encodeCsvStream enc . SMP.each
|
||||
|
||||
-- | Encode a collection to a CSV as a bytestring 'BB.Builder'.
|
||||
encodeCsvUtf8 :: (Foldable f, CE.Headedness h)
|
||||
=> CE.Colonnade h a ByteString -- ^ Tablular encoding
|
||||
-> f a -- ^ Value of each row
|
||||
-> BB.Builder
|
||||
encodeCsvUtf8 enc =
|
||||
streamToBuilder . encodeCsvStreamUtf8 enc . SMP.each
|
||||
|
||||
streamToBuilder :: Stream (Of ByteString) Identity () -> BB.Builder
|
||||
streamToBuilder s = SM.destroy s
|
||||
(\(bs :> bb) -> BB.byteString bs <> bb) runIdentity (\() -> mempty)
|
||||
|
||||
textStreamToBuilder :: Stream (Of Text) Identity () -> TB.Builder
|
||||
textStreamToBuilder s = SM.destroy s
|
||||
(\(bs :> bb) -> TB.fromText bs <> bb) runIdentity (\() -> mempty)
|
||||
|
||||
encodeCsvInternal :: (Monad m, CE.Headedness h)
|
||||
=> (c -> Escaped c)
|
||||
-> c -- ^ separator
|
||||
-> c -- ^ newline
|
||||
-> CE.Colonnade h a c
|
||||
-> Stream (Of a) m r
|
||||
-> Stream (Of c) m r
|
||||
encodeCsvInternal escapeFunc separatorStr newlineStr colonnade s = do
|
||||
case CE.headednessExtract of
|
||||
Just toContent -> encodeHeader toContent escapeFunc separatorStr newlineStr colonnade
|
||||
Nothing -> return ()
|
||||
encodeRows escapeFunc separatorStr newlineStr colonnade s
|
||||
|
||||
encodeHeader :: Monad m
|
||||
=> (h c -> c)
|
||||
-> (c -> Escaped c)
|
||||
-> c -- ^ separator
|
||||
-> c -- ^ newline
|
||||
-> CE.Colonnade h a c
|
||||
-> Stream (Of c) m ()
|
||||
encodeHeader toContent escapeFunc separatorStr newlineStr colonnade = do
|
||||
let (vs,ws) = V.splitAt 1 (CE.getColonnade colonnade)
|
||||
-- we only need to do this split because the first cell
|
||||
-- gets treated differently than the others. It does not
|
||||
-- get a separator added before it.
|
||||
V.forM_ vs $ \(CE.OneColonnade h _) -> do
|
||||
SMP.yield (getEscaped (escapeFunc (toContent h)))
|
||||
V.forM_ ws $ \(CE.OneColonnade h _) -> do
|
||||
SMP.yield separatorStr
|
||||
SMP.yield (getEscaped (escapeFunc (toContent h)))
|
||||
SMP.yield newlineStr
|
||||
|
||||
mapStreamM :: Monad m
|
||||
=> (a -> Stream (Of b) m x)
|
||||
-> Stream (Of a) m r
|
||||
-> Stream (Of b) m r
|
||||
mapStreamM f = SM.concats . SM.mapsM (\(a :> s) -> return (f a >> return s))
|
||||
|
||||
encodeRows :: Monad m
|
||||
=> (c -> Escaped c)
|
||||
-> c -- ^ separator
|
||||
-> c -- ^ newline
|
||||
-> CE.Colonnade f a c
|
||||
-> Stream (Of a) m r
|
||||
-> Stream (Of c) m r
|
||||
encodeRows escapeFunc separatorStr newlineStr colonnade = mapStreamM $ \a -> do
|
||||
let (vs,ws) = V.splitAt 1 (CE.getColonnade colonnade)
|
||||
-- we only need to do this split because the first cell
|
||||
-- gets treated differently than the others. It does not
|
||||
-- get a separator added before it.
|
||||
V.forM_ vs $ \(CE.OneColonnade _ encode) -> SMP.yield (getEscaped (escapeFunc (encode a)))
|
||||
V.forM_ ws $ \(CE.OneColonnade _ encode) -> do
|
||||
SMP.yield separatorStr
|
||||
SMP.yield (getEscaped (escapeFunc (encode a)))
|
||||
SMP.yield newlineStr
|
||||
|
||||
data IndexedHeader a = IndexedHeader
|
||||
{ indexedHeaderIndexed :: {-# UNPACK #-} !Int
|
||||
, indexedHeaderHeader :: !a
|
||||
}
|
||||
|
||||
-- | Maps over a 'Decolonnade' that expects headers, converting these
|
||||
-- expected headers into the indices of the columns that they
|
||||
-- correspond to.
|
||||
headedToIndexed :: forall c a. Eq c
|
||||
=> (c -> T.Text)
|
||||
-> Vector c -- ^ Headers in the source document
|
||||
-> Siphon CE.Headed c a -- ^ Decolonnade that contains expected headers
|
||||
-> Either SiphonError (Siphon IndexedHeader c a)
|
||||
headedToIndexed toStr v =
|
||||
mapLeft (\(HeaderErrors a b c) -> SiphonError 0 (RowErrorHeaders a b c))
|
||||
. getEitherWrap
|
||||
. go
|
||||
where
|
||||
go :: forall b.
|
||||
Siphon CE.Headed c b
|
||||
-> EitherWrap HeaderErrors (Siphon IndexedHeader c b)
|
||||
go (SiphonPure b) = EitherWrap (Right (SiphonPure b))
|
||||
go (SiphonAp (CE.Headed h) decode apNext) =
|
||||
let rnext = go apNext
|
||||
ixs = V.elemIndices h v
|
||||
ixsLen = V.length ixs
|
||||
rcurrent
|
||||
| ixsLen == 1 = Right (ixs V.! 0) -- (V.unsafeIndex ixs 0)
|
||||
| ixsLen == 0 = Left (HeaderErrors V.empty (V.singleton (toStr h)) V.empty)
|
||||
| otherwise =
|
||||
let dups = V.singleton (V.map (\ix -> CellError ix (toStr (v V.! ix) {- (V.unsafeIndex v ix) -} )) ixs)
|
||||
in Left (HeaderErrors dups V.empty V.empty)
|
||||
in (\ix nextSiphon -> SiphonAp (IndexedHeader ix h) decode nextSiphon)
|
||||
<$> EitherWrap rcurrent
|
||||
<*> rnext
|
||||
|
||||
data HeaderErrors = HeaderErrors !(Vector (Vector CellError)) !(Vector T.Text) !(Vector Int)
|
||||
|
||||
instance Semigroup HeaderErrors where
|
||||
HeaderErrors a1 b1 c1 <> HeaderErrors a2 b2 c2 = HeaderErrors
|
||||
(mappend a1 a2) (mappend b1 b2) (mappend c1 c2)
|
||||
|
||||
instance Monoid HeaderErrors where
|
||||
mempty = HeaderErrors mempty mempty mempty
|
||||
mappend = (SG.<>)
|
||||
|
||||
-- byteStringChar8 :: Siphon ByteString
|
||||
-- byteStringChar8 = Siphon
|
||||
-- escape
|
||||
-- encodeRow
|
||||
-- (A.parse (row comma))
|
||||
-- B.null
|
||||
|
||||
escapeChar8 :: ByteString -> Escaped ByteString
|
||||
escapeChar8 t = case B.find (\c -> c == newline || c == cr || c == comma || c == doubleQuote) t of
|
||||
Nothing -> Escaped t
|
||||
Just _ -> escapeAlways t
|
||||
|
||||
textEscapeChar8 :: Text -> Escaped Text
|
||||
textEscapeChar8 t = case T.find (\c -> c == '\n' || c == '\r' || c == ',' || c == '"') t of
|
||||
Nothing -> Escaped t
|
||||
Just _ -> textEscapeAlways t
|
||||
|
||||
-- This implementation is definitely suboptimal.
|
||||
-- A better option (which would waste a little space
|
||||
-- but would be much faster) would be to build the
|
||||
-- new bytestring by writing to a buffer directly.
|
||||
escapeAlways :: ByteString -> Escaped ByteString
|
||||
escapeAlways t = Escaped $ LByteString.toStrict $ Builder.toLazyByteString $
|
||||
Builder.word8 doubleQuote
|
||||
<> B.foldl
|
||||
(\ acc b -> acc <> if b == doubleQuote
|
||||
then Builder.byteString
|
||||
(B.pack [doubleQuote,doubleQuote])
|
||||
else Builder.word8 b)
|
||||
mempty
|
||||
t
|
||||
<> Builder.word8 doubleQuote
|
||||
|
||||
-- Suboptimal for similar reason as escapeAlways.
|
||||
textEscapeAlways :: Text -> Escaped Text
|
||||
textEscapeAlways t = Escaped $ LT.toStrict $ TB.toLazyText $
|
||||
TB.singleton '"'
|
||||
<> T.foldl
|
||||
(\ acc b -> acc <> if b == '"'
|
||||
then TB.fromString "\"\""
|
||||
else TB.singleton b
|
||||
)
|
||||
mempty
|
||||
t
|
||||
<> TB.singleton '"'
|
||||
|
||||
-- Parse a record, not including the terminating line separator. The
|
||||
-- terminating line separate is not included as the last record in a
|
||||
-- CSV file is allowed to not have a terminating line separator. You
|
||||
-- most likely want to use the 'endOfLine' parser in combination with
|
||||
-- this parser.
|
||||
--
|
||||
-- row :: Word8 -- ^ Field delimiter
|
||||
-- -> AL.Parser (Vector ByteString)
|
||||
-- row !delim = rowNoNewline delim <* endOfLine
|
||||
-- {-# INLINE row #-}
|
||||
--
|
||||
-- rowNoNewline :: Word8 -- ^ Field delimiter
|
||||
-- -> AL.Parser (Vector ByteString)
|
||||
-- rowNoNewline !delim = V.fromList <$!> field delim `sepByDelim1'` delim
|
||||
-- {-# INLINE rowNoNewline #-}
|
||||
--
|
||||
-- removeBlankLines :: [Vector ByteString] -> [Vector ByteString]
|
||||
-- removeBlankLines = filter (not . blankLine)
|
||||
|
||||
|
||||
-- | Parse a field. The field may be in either the escaped or
|
||||
-- non-escaped format. The return value is unescaped. This
|
||||
-- parser will consume the comma that comes after a field
|
||||
-- but not a newline that follows a field. If we are positioned
|
||||
-- at a newline when it starts, that newline will be consumed
|
||||
-- and we return CellResultNewline.
|
||||
field :: Word8 -> AL.Parser (CellResult ByteString)
|
||||
field !delim = do
|
||||
mb <- A.peekWord8
|
||||
-- We purposely don't use <|> as we want to commit to the first
|
||||
-- choice if we see a double quote.
|
||||
case mb of
|
||||
Just b
|
||||
| b == doubleQuote -> do
|
||||
(bs,tc) <- escapedField
|
||||
case tc of
|
||||
TrailCharComma -> return (CellResultData bs)
|
||||
TrailCharNewline -> return (CellResultNewline bs EndedNo)
|
||||
TrailCharEnd -> return (CellResultNewline bs EndedYes)
|
||||
| b == 10 || b == 13 -> do
|
||||
_ <- eatNewlines
|
||||
isEnd <- A.atEnd
|
||||
if isEnd
|
||||
then return (CellResultNewline B.empty EndedYes)
|
||||
else return (CellResultNewline B.empty EndedNo)
|
||||
| otherwise -> do
|
||||
(bs,tc) <- unescapedField delim
|
||||
case tc of
|
||||
TrailCharComma -> return (CellResultData bs)
|
||||
TrailCharNewline -> return (CellResultNewline bs EndedNo)
|
||||
TrailCharEnd -> return (CellResultNewline bs EndedYes)
|
||||
Nothing -> return (CellResultNewline B.empty EndedYes)
|
||||
{-# INLINE field #-}
|
||||
|
||||
eatNewlines :: AL.Parser S.ByteString
|
||||
eatNewlines = A.takeWhile (\x -> x == 10 || x == 13)
|
||||
|
||||
escapedField :: AL.Parser (S.ByteString,TrailChar)
|
||||
escapedField = do
|
||||
_ <- dquote
|
||||
-- The scan state is 'True' if the previous character was a double
|
||||
-- quote. We need to drop a trailing double quote left by scan.
|
||||
s <- S.init <$>
|
||||
( A.scan False $ \s c ->
|
||||
if c == doubleQuote
|
||||
then Just (not s)
|
||||
else if s
|
||||
then Nothing
|
||||
else Just False
|
||||
)
|
||||
mb <- A.peekWord8
|
||||
trailChar <- case mb of
|
||||
Just b
|
||||
| b == comma -> A.anyWord8 >> return TrailCharComma
|
||||
| b == newline || b == cr -> A.anyWord8 >> return TrailCharNewline
|
||||
| otherwise -> fail "encountered double quote after escaped field"
|
||||
Nothing -> return TrailCharEnd
|
||||
if doubleQuote `S.elem` s
|
||||
then case Z.parse unescape s of
|
||||
Right r -> return (r,trailChar)
|
||||
Left err -> fail err
|
||||
else return (s,trailChar)
|
||||
|
||||
data TrailChar = TrailCharNewline | TrailCharComma | TrailCharEnd
|
||||
|
||||
-- | Consume an unescaped field. If it ends with a newline,
|
||||
-- leave that in tact. If it ends with a comma, consume the comma.
|
||||
unescapedField :: Word8 -> AL.Parser (S.ByteString,TrailChar)
|
||||
unescapedField !delim = do
|
||||
bs <- A.takeWhile $ \c ->
|
||||
c /= doubleQuote &&
|
||||
c /= newline &&
|
||||
c /= delim &&
|
||||
c /= cr
|
||||
mb <- A.peekWord8
|
||||
case mb of
|
||||
Just b
|
||||
| b == comma -> A.anyWord8 >> return (bs,TrailCharComma)
|
||||
| b == newline || b == cr -> A.anyWord8 >> return (bs,TrailCharNewline)
|
||||
| otherwise -> fail "encountered double quote in unescaped field"
|
||||
Nothing -> return (bs,TrailCharEnd)
|
||||
|
||||
dquote :: AL.Parser Char
|
||||
dquote = char '"'
|
||||
|
||||
-- | This could be improved. We could avoid the builder and just
|
||||
-- write to a buffer directly.
|
||||
unescape :: Z.Parser S.ByteString
|
||||
unescape = (LByteString.toStrict . toLazyByteString) <$!> go mempty where
|
||||
go acc = do
|
||||
h <- Z.takeWhile (/= doubleQuote)
|
||||
let rest = do
|
||||
start <- Z.take 2
|
||||
if (S.unsafeHead start == doubleQuote &&
|
||||
S.unsafeIndex start 1 == doubleQuote)
|
||||
then go (acc `mappend` byteString h `mappend` byteString (BC8.singleton '"'))
|
||||
else fail "invalid CSV escape sequence"
|
||||
done <- Z.atEnd
|
||||
if done
|
||||
then return (acc `mappend` byteString h)
|
||||
else rest
|
||||
|
||||
-- | Is this an empty record (i.e. a blank line)?
|
||||
blankLine :: V.Vector B.ByteString -> Bool
|
||||
blankLine v = V.length v == 1 && (B.null (V.head v))
|
||||
|
||||
doubleQuote, newline, cr, comma :: Word8
|
||||
doubleQuote = 34
|
||||
newline = 10
|
||||
cr = 13
|
||||
comma = 44
|
||||
|
||||
-- | This adds one to the index because text editors consider
|
||||
-- line number to be one-based, not zero-based.
|
||||
humanizeSiphonError :: SiphonError -> String
|
||||
humanizeSiphonError (SiphonError ix e) = unlines
|
||||
$ ("Decolonnade error on line " ++ show (ix + 1) ++ " of file.")
|
||||
: ("Error Category: " ++ descr)
|
||||
: map (" " ++) errDescrs
|
||||
where (descr,errDescrs) = prettyRowError e
|
||||
|
||||
prettyRowError :: RowError -> (String, [String])
|
||||
prettyRowError x = case x of
|
||||
RowErrorParse -> (,) "CSV Parsing"
|
||||
[ "The cells were malformed."
|
||||
]
|
||||
RowErrorSize reqLen actualLen -> (,) "Row Length"
|
||||
[ "Expected the row to have exactly " ++ show reqLen ++ " cells."
|
||||
, "The row only has " ++ show actualLen ++ " cells."
|
||||
]
|
||||
RowErrorHeaderSize reqLen actualLen -> (,) "Minimum Header Length"
|
||||
[ "Expected the row to have at least " ++ show reqLen ++ " cells."
|
||||
, "The row only has " ++ show actualLen ++ " cells."
|
||||
]
|
||||
RowErrorMalformed column -> (,) "Text Decolonnade"
|
||||
[ "Tried to decode input input in column " ++ columnNumToLetters column ++ " text"
|
||||
, "There is a mistake in the encoding of the text."
|
||||
]
|
||||
RowErrorHeaders dupErrs namedErrs unnamedErrs -> (,) "Missing Headers" $ concat
|
||||
[ if V.length namedErrs > 0 then prettyNamedMissingHeaders namedErrs else []
|
||||
, if V.length unnamedErrs > 0 then ["Missing unnamed headers"] else []
|
||||
, if V.length dupErrs > 0 then prettyHeadingErrors dupErrs else []
|
||||
]
|
||||
RowErrorDecode errs -> (,) "Cell Decolonnade" (prettyCellErrors errs)
|
||||
|
||||
prettyCellErrors :: Vector CellError -> [String]
|
||||
prettyCellErrors errs = drop 1 $
|
||||
flip concatMap errs $ \(CellError ix content) ->
|
||||
let str = T.unpack content in
|
||||
[ "-----------"
|
||||
, "Column " ++ columnNumToLetters ix
|
||||
, "Cell Content Length: " ++ show (Prelude.length str)
|
||||
, "Cell Content: " ++ if null str
|
||||
then "[empty cell]"
|
||||
else str
|
||||
]
|
||||
|
||||
prettyNamedMissingHeaders :: Vector T.Text -> [String]
|
||||
prettyNamedMissingHeaders missing = concat
|
||||
[ concatMap (\h -> ["The header " ++ T.unpack h ++ " was missing."]) missing
|
||||
]
|
||||
|
||||
prettyHeadingErrors :: Vector (Vector CellError) -> [String]
|
||||
prettyHeadingErrors missing = join (V.toList (fmap f missing))
|
||||
where
|
||||
f :: Vector CellError -> [String]
|
||||
f v
|
||||
| not (V.null w) && V.all (== V.head w) (V.tail w) =
|
||||
[ "The header ["
|
||||
, T.unpack (V.head w)
|
||||
, "] appears in columns "
|
||||
, L.intercalate ", " (V.toList (V.map (\(CellError ix _) -> columnNumToLetters ix) v))
|
||||
]
|
||||
| otherwise = multiMsg : V.toList
|
||||
(V.map (\(CellError ix content) -> " Column " ++ columnNumToLetters ix ++ ": " ++ T.unpack content) v)
|
||||
where
|
||||
w :: Vector T.Text
|
||||
w = V.map cellErrorContent v
|
||||
multiMsg :: String
|
||||
multiMsg = "Multiple headers matched the same predicate:"
|
||||
|
||||
columnNumToLetters :: Int -> String
|
||||
columnNumToLetters i
|
||||
| i >= 0 && i < 25 = [chr (i + 65)]
|
||||
| otherwise = "Beyond Z. Fix this."
|
||||
|
||||
newtype EitherWrap a b = EitherWrap
|
||||
{ getEitherWrap :: Either a b
|
||||
} deriving (Functor)
|
||||
|
||||
instance Monoid a => Applicative (EitherWrap a) where
|
||||
pure = EitherWrap . Right
|
||||
EitherWrap (Left a1) <*> EitherWrap (Left a2) = EitherWrap (Left (mappend a1 a2))
|
||||
EitherWrap (Left a1) <*> EitherWrap (Right _) = EitherWrap (Left a1)
|
||||
EitherWrap (Right _) <*> EitherWrap (Left a2) = EitherWrap (Left a2)
|
||||
EitherWrap (Right f) <*> EitherWrap (Right b) = EitherWrap (Right (f b))
|
||||
|
||||
mapLeft :: (a -> b) -> Either a c -> Either b c
|
||||
mapLeft _ (Right a) = Right a
|
||||
mapLeft f (Left a) = Left (f a)
|
||||
|
||||
consumeHeaderRowUtf8 :: Monad m
|
||||
=> Stream (Of ByteString) m ()
|
||||
-> m (Either SiphonError (Of (Vector ByteString) (Stream (Of ByteString) m ())))
|
||||
consumeHeaderRowUtf8 = consumeHeaderRow (A.parse (field comma)) B.null B.empty (\() -> True)
|
||||
|
||||
consumeBodyUtf8 :: forall m a. Monad m
|
||||
=> Int -- ^ index of first row, usually zero or one
|
||||
-> Int -- ^ Required row length
|
||||
-> Siphon IndexedHeader ByteString a
|
||||
-> Stream (Of ByteString) m ()
|
||||
-> Stream (Of a) m (Maybe SiphonError)
|
||||
consumeBodyUtf8 = consumeBody utf8ToStr
|
||||
(A.parse (field comma)) B.null B.empty (\() -> True)
|
||||
|
||||
utf8ToStr :: ByteString -> T.Text
|
||||
utf8ToStr = either (\_ -> T.empty) id . decodeUtf8'
|
||||
|
||||
consumeHeaderRow :: forall m r c. Monad m
|
||||
=> (c -> ATYP.IResult c (CellResult c))
|
||||
-> (c -> Bool) -- ^ true if null string
|
||||
-> c
|
||||
-> (r -> Bool) -- ^ true if termination is acceptable
|
||||
-> Stream (Of c) m r
|
||||
-> m (Either SiphonError (Of (Vector c) (Stream (Of c) m r)))
|
||||
consumeHeaderRow parseCell isNull emptyStr isGood s0 = go 0 StrictListNil s0
|
||||
where
|
||||
go :: Int
|
||||
-> StrictList c
|
||||
-> Stream (Of c) m r
|
||||
-> m (Either SiphonError (Of (Vector c) (Stream (Of c) m r)))
|
||||
go !cellsLen !cells !s1 = do
|
||||
e <- skipWhile isNull s1
|
||||
case e of
|
||||
Left r -> return $ if isGood r
|
||||
then Right (reverseVectorStrictList cellsLen cells :> return r)
|
||||
else Left (SiphonError 0 RowErrorParse)
|
||||
Right (c :> s2) -> handleResult cellsLen cells (parseCell c) s2
|
||||
handleResult :: Int -> StrictList c
|
||||
-> ATYP.IResult c (CellResult c)
|
||||
-> Stream (Of c) m r
|
||||
-> m (Either SiphonError (Of (Vector c) (Stream (Of c) m r)))
|
||||
handleResult !cellsLen !cells !result s1 = case result of
|
||||
ATYP.Fail _ _ _ -> return $ Left $ SiphonError 0 RowErrorParse
|
||||
ATYP.Done !c1 !res -> case res of
|
||||
-- it might be wrong to ignore whether or not the stream has ended
|
||||
CellResultNewline cd _ -> do
|
||||
let v = reverseVectorStrictList (cellsLen + 1) (StrictListCons cd cells)
|
||||
return (Right (v :> (SMP.yield c1 >> s1)))
|
||||
CellResultData !cd -> if isNull c1
|
||||
then go (cellsLen + 1) (StrictListCons cd cells) s1
|
||||
else handleResult (cellsLen + 1) (StrictListCons cd cells) (parseCell c1) s1
|
||||
ATYP.Partial k -> do
|
||||
e <- skipWhile isNull s1
|
||||
case e of
|
||||
Left r -> handleResult cellsLen cells (k emptyStr) (return r)
|
||||
Right (c1 :> s2) -> handleResult cellsLen cells (k c1) s2
|
||||
|
||||
consumeBody :: forall m r c a. Monad m
|
||||
=> (c -> T.Text)
|
||||
-> (c -> ATYP.IResult c (CellResult c))
|
||||
-> (c -> Bool)
|
||||
-> c
|
||||
-> (r -> Bool) -- ^ True if termination is acceptable. False if it is because of a decoding error.
|
||||
-> Int -- ^ index of first row, usually zero or one
|
||||
-> Int -- ^ Required row length
|
||||
-> Siphon IndexedHeader c a
|
||||
-> Stream (Of c) m r
|
||||
-> Stream (Of a) m (Maybe SiphonError)
|
||||
consumeBody toStr parseCell isNull emptyStr isGood row0 reqLen siphon s0 =
|
||||
go row0 0 StrictListNil s0
|
||||
where
|
||||
go :: Int -> Int -> StrictList c -> Stream (Of c) m r -> Stream (Of a) m (Maybe SiphonError)
|
||||
go !row !cellsLen !cells !s1 = do
|
||||
e <- lift (skipWhile isNull s1)
|
||||
case e of
|
||||
Left r -> return $ if isGood r
|
||||
then Nothing
|
||||
else Just (SiphonError row RowErrorParse)
|
||||
Right (c :> s2) -> handleResult row cellsLen cells (parseCell c) s2
|
||||
handleResult :: Int -> Int -> StrictList c
|
||||
-> ATYP.IResult c (CellResult c)
|
||||
-> Stream (Of c) m r
|
||||
-> Stream (Of a) m (Maybe SiphonError)
|
||||
handleResult !row !cellsLen !cells !result s1 = case result of
|
||||
ATYP.Fail _ _ _ -> return $ Just $ SiphonError row RowErrorParse
|
||||
ATYP.Done !c1 !res -> case res of
|
||||
CellResultNewline !cd !ended -> do
|
||||
case decodeRow row (reverseVectorStrictList (cellsLen + 1) (StrictListCons cd cells)) of
|
||||
Left err -> return (Just err)
|
||||
Right a -> do
|
||||
SMP.yield a
|
||||
case ended of
|
||||
EndedYes -> do
|
||||
e <- lift (SM.inspect s1)
|
||||
case e of
|
||||
Left r -> return $ if isGood r
|
||||
then Nothing
|
||||
else Just (SiphonError row RowErrorParse)
|
||||
Right _ -> error "siphon: logical error, stream should be exhausted"
|
||||
EndedNo -> if isNull c1
|
||||
then go (row + 1) 0 StrictListNil s1
|
||||
else handleResult (row + 1) 0 StrictListNil (parseCell c1) s1
|
||||
CellResultData !cd -> if isNull c1
|
||||
then go row (cellsLen + 1) (StrictListCons cd cells) s1
|
||||
else handleResult row (cellsLen + 1) (StrictListCons cd cells) (parseCell c1) s1
|
||||
ATYP.Partial k -> do
|
||||
e <- lift (skipWhile isNull s1)
|
||||
case e of
|
||||
Left r -> handleResult row cellsLen cells (k emptyStr) (return r)
|
||||
Right (c1 :> s2) -> handleResult row cellsLen cells (k c1) s2
|
||||
decodeRow :: Int -> Vector c -> Either SiphonError a
|
||||
decodeRow rowIx v =
|
||||
let vlen = V.length v in
|
||||
if vlen /= reqLen
|
||||
then Left $ SiphonError rowIx $ RowErrorSize reqLen vlen
|
||||
else uncheckedRunWithRow toStr rowIx siphon v
|
||||
|
||||
-- | You must pass the length of the list and as the first argument.
|
||||
-- Passing the wrong length will lead to an error.
|
||||
reverseVectorStrictList :: forall c. Int -> StrictList c -> Vector c
|
||||
reverseVectorStrictList len sl0 = V.create $ do
|
||||
mv <- MV.new len
|
||||
go1 mv
|
||||
return mv
|
||||
where
|
||||
go1 :: forall s. MVector s c -> ST s ()
|
||||
go1 !mv = go2 0 sl0
|
||||
where
|
||||
go2 :: Int -> StrictList c -> ST s ()
|
||||
go2 _ StrictListNil = return ()
|
||||
go2 !ix (StrictListCons c slNext) = do
|
||||
MV.write mv ix c
|
||||
go2 (ix + 1) slNext
|
||||
|
||||
|
||||
skipWhile :: forall m a r. Monad m
|
||||
=> (a -> Bool)
|
||||
-> Stream (Of a) m r
|
||||
-> m (Either r (Of a (Stream (Of a) m r)))
|
||||
skipWhile f = go where
|
||||
go :: Stream (Of a) m r
|
||||
-> m (Either r (Of a (Stream (Of a) m r)))
|
||||
go s1 = do
|
||||
e <- SM.inspect s1
|
||||
case e of
|
||||
Left _ -> return e
|
||||
Right (a :> s2) -> if f a
|
||||
then go s2
|
||||
else return e
|
||||
|
||||
-- | Strict in the spine and in the values
|
||||
data StrictList a = StrictListNil | StrictListCons !a !(StrictList a)
|
||||
|
||||
-- | This function uses 'unsafeIndex' to access
|
||||
-- elements of the 'Vector'.
|
||||
uncheckedRunWithRow ::
|
||||
(c -> T.Text)
|
||||
-> Int
|
||||
-> Siphon IndexedHeader c a
|
||||
-> Vector c
|
||||
-> Either SiphonError a
|
||||
uncheckedRunWithRow toStr i d v =
|
||||
mapLeft (SiphonError i . RowErrorDecode) (uncheckedRun toStr d v)
|
||||
|
||||
-- | This function does not check to make sure that the indicies in
|
||||
-- the 'Decolonnade' are in the 'Vector'. Only use this if you have
|
||||
-- already verified that none of the indices in the siphon are
|
||||
-- out of the bounds.
|
||||
uncheckedRun :: forall c a.
|
||||
(c -> T.Text)
|
||||
-> Siphon IndexedHeader c a
|
||||
-> Vector c
|
||||
-> Either (Vector CellError) a
|
||||
uncheckedRun toStr dc v = getEitherWrap (go dc)
|
||||
where
|
||||
go :: forall b.
|
||||
Siphon IndexedHeader c b
|
||||
-> EitherWrap (Vector CellError) b
|
||||
go (SiphonPure b) = EitherWrap (Right b)
|
||||
go (SiphonAp (IndexedHeader ix _) decode apNext) =
|
||||
let rnext = go apNext
|
||||
content = v V.! ix -- V.unsafeIndex v ix
|
||||
rcurrent = maybe
|
||||
(Left (V.singleton (CellError ix (toStr content))))
|
||||
Right
|
||||
(decode content)
|
||||
in rnext <*> (EitherWrap rcurrent)
|
||||
|
||||
siphonLength :: forall f c a. Siphon f c a -> Int
|
||||
siphonLength = go 0 where
|
||||
go :: forall b. Int -> Siphon f c b -> Int
|
||||
go !a (SiphonPure _) = a
|
||||
go !a (SiphonAp _ _ apNext) = go (a + 1) apNext
|
||||
|
||||
maxIndex :: forall c a. Siphon IndexedHeader c a -> Int
|
||||
maxIndex = go 0 where
|
||||
go :: forall b. Int -> Siphon IndexedHeader c b -> Int
|
||||
go !ix (SiphonPure _) = ix
|
||||
go !ix1 (SiphonAp (IndexedHeader ix2 _) _ apNext) =
|
||||
go (max ix1 ix2) apNext
|
||||
|
||||
-- | Uses the argument to parse a CSV column.
|
||||
headless :: (c -> Maybe a) -> Siphon CE.Headless c a
|
||||
headless f = SiphonAp CE.Headless f (SiphonPure id)
|
||||
|
||||
-- | Uses the second argument to parse a CSV column whose
|
||||
-- header content matches the first column exactly.
|
||||
headed :: c -> (c -> Maybe a) -> Siphon CE.Headed c a
|
||||
headed h f = SiphonAp (CE.Headed h) f (SiphonPure id)
|
||||
|
||||
-- | Uses the second argument to parse a CSV column that
|
||||
-- is positioned at the index given by the first argument.
|
||||
indexed :: Int -> (c -> Maybe a) -> Siphon Indexed c a
|
||||
indexed ix f = SiphonAp (Indexed ix) f (SiphonPure id)
|
||||
|
||||
-- $setup
|
||||
--
|
||||
-- This code is copied from the head section. It has to be
|
||||
-- run before every set of tests.
|
||||
--
|
||||
-- >>> :set -XOverloadedStrings
|
||||
-- >>> import Siphon (Siphon)
|
||||
-- >>> import Colonnade (Colonnade,Headed)
|
||||
-- >>> import qualified Siphon as S
|
||||
-- >>> import qualified Colonnade as C
|
||||
-- >>> import qualified Data.Text as T
|
||||
-- >>> import Data.Text (Text)
|
||||
-- >>> import qualified Data.Text.Lazy.IO as LTIO
|
||||
-- >>> import qualified Data.Text.Lazy.Builder as LB
|
||||
-- >>> import Data.Maybe (fromMaybe)
|
||||
-- >>> data Person = Person { name :: Text, age :: Int, company :: Maybe Text}
|
||||
|
||||
|
||||
@ -1,24 +1,35 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
|
||||
module Siphon.Decoding where
|
||||
module Siphon.Decoding
|
||||
( mkParseError
|
||||
, headlessPipe
|
||||
, indexedPipe
|
||||
, headedPipe
|
||||
, consumeGeneral
|
||||
, pipeGeneral
|
||||
, convertDecodeError
|
||||
) where
|
||||
|
||||
import Siphon.Types
|
||||
import Colonnade.Types
|
||||
import Colonnade (Headed(..),Headless(..))
|
||||
import Siphon.Internal (row,comma)
|
||||
import Data.Text (Text)
|
||||
import Data.ByteString (ByteString)
|
||||
import Pipes (yield,Pipe,Consumer',Producer,await)
|
||||
import Data.Vector (Vector)
|
||||
import Data.Functor.Contravariant (Contravariant(..))
|
||||
import Data.Char (chr)
|
||||
import qualified Data.Vector as Vector
|
||||
import qualified Colonnade.Decoding as Decoding
|
||||
import qualified Data.Attoparsec.ByteString as AttoByteString
|
||||
import qualified Data.ByteString.Char8 as ByteString
|
||||
import qualified Data.Attoparsec.Types as Atto
|
||||
|
||||
mkParseError :: Int -> [String] -> String -> DecodingRowError f content
|
||||
mkParseError :: Int -> [String] -> String -> DecolonnadeRowError f content
|
||||
mkParseError i ctxs msg = id
|
||||
$ DecodingRowError i
|
||||
$ DecolonnadeRowError i
|
||||
$ RowErrorParse $ concat
|
||||
[ "Contexts: ["
|
||||
, concat ctxs
|
||||
@ -28,37 +39,37 @@ mkParseError i ctxs msg = id
|
||||
]
|
||||
|
||||
-- | This is a convenience function for working with @pipes-text@.
|
||||
-- It will convert a UTF-8 decoding error into a `DecodingRowError`,
|
||||
-- It will convert a UTF-8 decoding error into a `DecolonnadeRowError`,
|
||||
-- so the pipes can be properly chained together.
|
||||
convertDecodeError :: String -> Either (Producer ByteString m ()) () -> Maybe (DecodingRowError f c)
|
||||
convertDecodeError encodingName (Left _) = Just (DecodingRowError 0 (RowErrorMalformed encodingName))
|
||||
convertDecodeError :: String -> Either (Producer ByteString m ()) () -> Maybe (DecolonnadeRowError f c)
|
||||
convertDecodeError encodingName (Left _) = Just (DecolonnadeRowError 0 (RowErrorMalformed encodingName))
|
||||
convertDecodeError _ (Right ()) = Nothing
|
||||
|
||||
-- | This is seldom useful but is included for completeness.
|
||||
headlessPipe :: Monad m
|
||||
=> Siphon c
|
||||
-> Decoding Headless c a
|
||||
-> Pipe c a m (DecodingRowError Headless c)
|
||||
-> Decolonnade Headless c a
|
||||
-> Pipe c a m (DecolonnadeRowError Headless c)
|
||||
headlessPipe sd decoding = uncheckedPipe requiredLength 0 sd indexedDecoding Nothing
|
||||
where
|
||||
indexedDecoding = Decoding.headlessToIndexed decoding
|
||||
requiredLength = Decoding.length indexedDecoding
|
||||
indexedDecoding = headlessToIndexed decoding
|
||||
requiredLength = decLength indexedDecoding
|
||||
|
||||
indexedPipe :: Monad m
|
||||
=> Siphon c
|
||||
-> Decoding (Indexed Headless) c a
|
||||
-> Pipe c a m (DecodingRowError Headless c)
|
||||
-> Decolonnade (Indexed Headless) c a
|
||||
-> Pipe c a m (DecolonnadeRowError Headless c)
|
||||
indexedPipe sd decoding = do
|
||||
e <- consumeGeneral 0 sd mkParseError
|
||||
case e of
|
||||
Left err -> return err
|
||||
Right (firstRow, mleftovers) ->
|
||||
let req = Decoding.maxIndex decoding
|
||||
let req = maxIndex decoding
|
||||
vlen = Vector.length firstRow
|
||||
in if vlen < req
|
||||
then return (DecodingRowError 0 (RowErrorMinSize req vlen))
|
||||
else case Decoding.uncheckedRun decoding firstRow of
|
||||
Left cellErr -> return $ DecodingRowError 0 $ RowErrorDecode cellErr
|
||||
then return (DecolonnadeRowError 0 (RowErrorMinSize req vlen))
|
||||
else case uncheckedRun decoding firstRow of
|
||||
Left cellErr -> return $ DecolonnadeRowError 0 $ RowErrorDecode cellErr
|
||||
Right a -> do
|
||||
yield a
|
||||
uncheckedPipe vlen 1 sd decoding mleftovers
|
||||
@ -66,15 +77,15 @@ indexedPipe sd decoding = do
|
||||
|
||||
headedPipe :: (Monad m, Eq c)
|
||||
=> Siphon c
|
||||
-> Decoding Headed c a
|
||||
-> Pipe c a m (DecodingRowError Headed c)
|
||||
-> Decolonnade Headed c a
|
||||
-> Pipe c a m (DecolonnadeRowError Headed c)
|
||||
headedPipe sd decoding = do
|
||||
e <- consumeGeneral 0 sd mkParseError
|
||||
case e of
|
||||
Left err -> return err
|
||||
Right (headers, mleftovers) ->
|
||||
case Decoding.headedToIndexed headers decoding of
|
||||
Left headingErrs -> return (DecodingRowError 0 (RowErrorHeading headingErrs))
|
||||
case headedToIndexed headers decoding of
|
||||
Left headingErrs -> return (DecolonnadeRowError 0 (RowErrorHeading headingErrs))
|
||||
Right indexedDecoding ->
|
||||
let requiredLength = Vector.length headers
|
||||
in uncheckedPipe requiredLength 1 sd indexedDecoding mleftovers
|
||||
@ -84,18 +95,18 @@ uncheckedPipe :: Monad m
|
||||
=> Int -- ^ expected length of each row
|
||||
-> Int -- ^ index of first row, usually zero or one
|
||||
-> Siphon c
|
||||
-> Decoding (Indexed f) c a
|
||||
-> Decolonnade (Indexed f) c a
|
||||
-> Maybe c
|
||||
-> Pipe c a m (DecodingRowError f c)
|
||||
-> Pipe c a m (DecolonnadeRowError f c)
|
||||
uncheckedPipe requiredLength ix sd d mleftovers =
|
||||
pipeGeneral ix sd mkParseError checkedRunWithRow mleftovers
|
||||
where
|
||||
checkedRunWithRow rowIx v =
|
||||
let vlen = Vector.length v in
|
||||
if vlen /= requiredLength
|
||||
then Left $ DecodingRowError rowIx
|
||||
then Left $ DecolonnadeRowError rowIx
|
||||
$ RowErrorSize requiredLength vlen
|
||||
else Decoding.uncheckedRunWithRow rowIx d v
|
||||
else uncheckedRunWithRow rowIx d v
|
||||
|
||||
consumeGeneral :: Monad m
|
||||
=> Int
|
||||
@ -157,4 +168,169 @@ awaitSkip f = go where
|
||||
a <- await
|
||||
if f a then go else return a
|
||||
|
||||
-- | Converts the content type of a 'Decolonnade'. The @'Contravariant' f@
|
||||
-- constraint means that @f@ can be 'Headless' but not 'Headed'.
|
||||
contramapContent :: forall c1 c2 f a. Contravariant f => (c2 -> c1) -> Decolonnade f c1 a -> Decolonnade f c2 a
|
||||
contramapContent f = go
|
||||
where
|
||||
go :: forall b. Decolonnade f c1 b -> Decolonnade f c2 b
|
||||
go (DecolonnadePure x) = DecolonnadePure x
|
||||
go (DecolonnadeAp h decode apNext) =
|
||||
DecolonnadeAp (contramap f h) (decode . f) (go apNext)
|
||||
|
||||
headless :: (content -> Either String a) -> Decolonnade Headless content a
|
||||
headless f = DecolonnadeAp Headless f (DecolonnadePure id)
|
||||
|
||||
headed :: content -> (content -> Either String a) -> Decolonnade Headed content a
|
||||
headed h f = DecolonnadeAp (Headed h) f (DecolonnadePure id)
|
||||
|
||||
indexed :: Int -> (content -> Either String a) -> Decolonnade (Indexed Headless) content a
|
||||
indexed ix f = DecolonnadeAp (Indexed ix Headless) f (DecolonnadePure id)
|
||||
|
||||
maxIndex :: forall f c a. Decolonnade (Indexed f) c a -> Int
|
||||
maxIndex = go 0 where
|
||||
go :: forall b. Int -> Decolonnade (Indexed f) c b -> Int
|
||||
go !ix (DecolonnadePure _) = ix
|
||||
go !ix1 (DecolonnadeAp (Indexed ix2 _) decode apNext) =
|
||||
go (max ix1 ix2) apNext
|
||||
|
||||
-- | This function uses 'unsafeIndex' to access
|
||||
-- elements of the 'Vector'.
|
||||
uncheckedRunWithRow ::
|
||||
Int
|
||||
-> Decolonnade (Indexed f) content a
|
||||
-> Vector content
|
||||
-> Either (DecolonnadeRowError f content) a
|
||||
uncheckedRunWithRow i d v = mapLeft (DecolonnadeRowError i . RowErrorDecode) (uncheckedRun d v)
|
||||
|
||||
-- | This function does not check to make sure that the indicies in
|
||||
-- the 'Decolonnade' are in the 'Vector'.
|
||||
uncheckedRun :: forall content a f.
|
||||
Decolonnade (Indexed f) content a
|
||||
-> Vector content
|
||||
-> Either (DecolonnadeCellErrors f content) a
|
||||
uncheckedRun dc v = getEitherWrap (go dc)
|
||||
where
|
||||
go :: forall b.
|
||||
Decolonnade (Indexed f) content b
|
||||
-> EitherWrap (DecolonnadeCellErrors f content) b
|
||||
go (DecolonnadePure b) = EitherWrap (Right b)
|
||||
go (DecolonnadeAp ixed@(Indexed ix h) decode apNext) =
|
||||
let rnext = go apNext
|
||||
content = Vector.unsafeIndex v ix
|
||||
rcurrent = mapLeft (DecolonnadeCellErrors . Vector.singleton . DecolonnadeCellError content ixed) (decode content)
|
||||
in rnext <*> (EitherWrap rcurrent)
|
||||
|
||||
headlessToIndexed :: forall c a.
|
||||
Decolonnade Headless c a -> Decolonnade (Indexed Headless) c a
|
||||
headlessToIndexed = go 0 where
|
||||
go :: forall b. Int -> Decolonnade Headless c b -> Decolonnade (Indexed Headless) c b
|
||||
go !ix (DecolonnadePure a) = DecolonnadePure a
|
||||
go !ix (DecolonnadeAp Headless decode apNext) =
|
||||
DecolonnadeAp (Indexed ix Headless) decode (go (ix + 1) apNext)
|
||||
|
||||
decLength :: forall f c a. Decolonnade f c a -> Int
|
||||
decLength = go 0 where
|
||||
go :: forall b. Int -> Decolonnade f c b -> Int
|
||||
go !a (DecolonnadePure _) = a
|
||||
go !a (DecolonnadeAp _ _ apNext) = go (a + 1) apNext
|
||||
|
||||
-- | Maps over a 'Decolonnade' that expects headers, converting these
|
||||
-- expected headers into the indices of the columns that they
|
||||
-- correspond to.
|
||||
headedToIndexed :: forall content a. Eq content
|
||||
=> Vector content -- ^ Headers in the source document
|
||||
-> Decolonnade Headed content a -- ^ Decolonnade that contains expected headers
|
||||
-> Either (HeadingErrors content) (Decolonnade (Indexed Headed) content a)
|
||||
headedToIndexed v = getEitherWrap . go
|
||||
where
|
||||
go :: forall b. Eq content
|
||||
=> Decolonnade Headed content b
|
||||
-> EitherWrap (HeadingErrors content) (Decolonnade (Indexed Headed) content b)
|
||||
go (DecolonnadePure b) = EitherWrap (Right (DecolonnadePure b))
|
||||
go (DecolonnadeAp hd@(Headed h) decode apNext) =
|
||||
let rnext = go apNext
|
||||
ixs = Vector.elemIndices h v
|
||||
ixsLen = Vector.length ixs
|
||||
rcurrent
|
||||
| ixsLen == 1 = Right (Vector.unsafeIndex ixs 0)
|
||||
| ixsLen == 0 = Left (HeadingErrors (Vector.singleton h) Vector.empty)
|
||||
| otherwise = Left (HeadingErrors Vector.empty (Vector.singleton (h,ixsLen)))
|
||||
in (\ix ap -> DecolonnadeAp (Indexed ix hd) decode ap)
|
||||
<$> EitherWrap rcurrent
|
||||
<*> rnext
|
||||
|
||||
-- | This adds one to the index because text editors consider
|
||||
-- line number to be one-based, not zero-based.
|
||||
prettyError :: (c -> String) -> DecolonnadeRowError f c -> String
|
||||
prettyError toStr (DecolonnadeRowError ix e) = unlines
|
||||
$ ("Decolonnade error on line " ++ show (ix + 1) ++ " of file.")
|
||||
: ("Error Category: " ++ descr)
|
||||
: map (" " ++) errDescrs
|
||||
where (descr,errDescrs) = prettyRowError toStr e
|
||||
|
||||
prettyRowError :: (content -> String) -> RowError f content -> (String, [String])
|
||||
prettyRowError toStr x = case x of
|
||||
RowErrorParse err -> (,) "CSV Parsing"
|
||||
[ "The line could not be parsed into cells correctly."
|
||||
, "Original parser error: " ++ err
|
||||
]
|
||||
RowErrorSize reqLen actualLen -> (,) "Row Length"
|
||||
[ "Expected the row to have exactly " ++ show reqLen ++ " cells."
|
||||
, "The row only has " ++ show actualLen ++ " cells."
|
||||
]
|
||||
RowErrorMinSize reqLen actualLen -> (,) "Row Min Length"
|
||||
[ "Expected the row to have at least " ++ show reqLen ++ " cells."
|
||||
, "The row only has " ++ show actualLen ++ " cells."
|
||||
]
|
||||
RowErrorMalformed enc -> (,) "Text Decolonnade"
|
||||
[ "Tried to decode the input as " ++ enc ++ " text"
|
||||
, "There is a mistake in the encoding of the text."
|
||||
]
|
||||
RowErrorHeading errs -> (,) "Header" (prettyHeadingErrors toStr errs)
|
||||
RowErrorDecode errs -> (,) "Cell Decolonnade" (prettyCellErrors toStr errs)
|
||||
|
||||
prettyCellErrors :: (c -> String) -> DecolonnadeCellErrors f c -> [String]
|
||||
prettyCellErrors toStr (DecolonnadeCellErrors errs) = drop 1 $
|
||||
flip concatMap errs $ \(DecolonnadeCellError content (Indexed ix _) msg) ->
|
||||
let str = toStr content in
|
||||
[ "-----------"
|
||||
, "Column " ++ columnNumToLetters ix
|
||||
, "Original parse error: " ++ msg
|
||||
, "Cell Content Length: " ++ show (Prelude.length str)
|
||||
, "Cell Content: " ++ if null str
|
||||
then "[empty cell]"
|
||||
else str
|
||||
]
|
||||
|
||||
prettyHeadingErrors :: (c -> String) -> HeadingErrors c -> [String]
|
||||
prettyHeadingErrors conv (HeadingErrors missing duplicates) = concat
|
||||
[ concatMap (\h -> ["The header " ++ conv h ++ " was missing."]) missing
|
||||
, concatMap (\(h,n) -> ["The header " ++ conv h ++ " occurred " ++ show n ++ " times."]) duplicates
|
||||
]
|
||||
|
||||
columnNumToLetters :: Int -> String
|
||||
columnNumToLetters i
|
||||
| i >= 0 && i < 25 = [chr (i + 65)]
|
||||
| otherwise = "Beyond Z. Fix this."
|
||||
|
||||
|
||||
newtype EitherWrap a b = EitherWrap
|
||||
{ getEitherWrap :: Either a b
|
||||
} deriving (Functor)
|
||||
|
||||
instance Monoid a => Applicative (EitherWrap a) where
|
||||
pure = EitherWrap . Right
|
||||
EitherWrap (Left a1) <*> EitherWrap (Left a2) = EitherWrap (Left (mappend a1 a2))
|
||||
EitherWrap (Left a1) <*> EitherWrap (Right _) = EitherWrap (Left a1)
|
||||
EitherWrap (Right _) <*> EitherWrap (Left a2) = EitherWrap (Left a2)
|
||||
EitherWrap (Right f) <*> EitherWrap (Right b) = EitherWrap (Right (f b))
|
||||
|
||||
mapLeft :: (a -> b) -> Either a c -> Either b c
|
||||
mapLeft _ (Right a) = Right a
|
||||
mapLeft f (Left a) = Left (f a)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,33 +1,28 @@
|
||||
module Siphon.Encoding where
|
||||
|
||||
import Siphon.Types
|
||||
import Colonnade.Types
|
||||
import Colonnade (Colonnade,Headed)
|
||||
import Pipes (Pipe,yield)
|
||||
import qualified Pipes.Prelude as Pipes
|
||||
import qualified Colonnade.Encoding as Encoding
|
||||
import qualified Colonnade.Encode as E
|
||||
|
||||
row :: Siphon c
|
||||
-> Encoding f c a
|
||||
-> a
|
||||
-> c
|
||||
row :: Siphon c -> Colonnade f a c -> a -> c
|
||||
row (Siphon escape intercalate _ _) e =
|
||||
intercalate . Encoding.runRow escape e
|
||||
intercalate . E.row escape e
|
||||
|
||||
header :: Siphon c
|
||||
-> Encoding Headed c a
|
||||
-> c
|
||||
header :: Siphon c -> Colonnade Headed a c -> c
|
||||
header (Siphon escape intercalate _ _) e =
|
||||
intercalate (Encoding.runHeader escape e)
|
||||
intercalate (E.header escape e)
|
||||
|
||||
pipe :: Monad m
|
||||
=> Siphon c
|
||||
-> Encoding f c a
|
||||
-> Colonnade f a c
|
||||
-> Pipe a c m x
|
||||
pipe siphon encoding = Pipes.map (row siphon encoding)
|
||||
|
||||
headedPipe :: Monad m
|
||||
=> Siphon c
|
||||
-> Encoding Headed c a
|
||||
-> Colonnade Headed a c
|
||||
-> Pipe a c m x
|
||||
headedPipe siphon encoding = do
|
||||
yield (header siphon encoding)
|
||||
|
||||
@ -1,45 +1,77 @@
|
||||
module Siphon.Types where
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
{-# OPTIONS_GHC -Wall -Werror #-}
|
||||
|
||||
module Siphon.Types
|
||||
( Siphon(..)
|
||||
, Indexed(..)
|
||||
, SiphonError(..)
|
||||
, RowError(..)
|
||||
, CellError(..)
|
||||
) where
|
||||
|
||||
import Data.Vector (Vector)
|
||||
import Colonnade.Types (DecodingRowError)
|
||||
import qualified Data.Attoparsec.Types as Atto
|
||||
import Control.Exception (Exception)
|
||||
import Data.Text (Text)
|
||||
|
||||
newtype Escaped c = Escaped { getEscaped :: c }
|
||||
data CellError = CellError
|
||||
{ cellErrorColumn :: !Int
|
||||
, cellErrorContent :: !Text
|
||||
} deriving (Show,Read,Eq)
|
||||
|
||||
data Siphon c = Siphon
|
||||
{ siphonEscape :: !(c -> Escaped c)
|
||||
, siphonIntercalate :: !(Vector (Escaped c) -> c)
|
||||
, siphonParseRow :: c -> Atto.IResult c (Vector c)
|
||||
, siphonNull :: c -> Bool
|
||||
}
|
||||
newtype Indexed a = Indexed
|
||||
{ indexedIndex :: Int
|
||||
} deriving (Eq,Ord,Functor,Show,Read)
|
||||
|
||||
-- -- | This type is provided for convenience with @pipes-text@
|
||||
-- data CsvResult f c
|
||||
-- = CsvResultSuccess
|
||||
-- | CsvResultTextDecodeError
|
||||
-- | CsvResultDecodeError (DecodingRowError f c)
|
||||
-- deriving (Show,Read,Eq)
|
||||
data SiphonError = SiphonError
|
||||
{ siphonErrorRow :: !Int
|
||||
, siphonErrorCause :: !RowError
|
||||
} deriving (Show,Read,Eq)
|
||||
|
||||
instance Exception SiphonError
|
||||
|
||||
-- | Consider changing out the use of 'Vector' here
|
||||
-- with the humble list instead. It might fuse away
|
||||
-- better. Not sure though.
|
||||
-- data SiphonX c1 c2 = SiphonX
|
||||
-- { siphonXEscape :: !(c1 -> Escaped c2)
|
||||
-- , siphonXIntercalate :: !(Vector (Escaped c2) -> c2)
|
||||
-- }
|
||||
--
|
||||
-- data SiphonDecoding c1 c2 = SiphonDecoding
|
||||
-- { siphonDecodingParse :: c1 -> Atto.IResult c1 (Vector c2)
|
||||
-- , siphonDecodingNull :: c1 -> Bool
|
||||
-- }
|
||||
data RowError
|
||||
= RowErrorParse
|
||||
-- ^ Error occurred parsing the document into cells
|
||||
| RowErrorDecode !(Vector CellError)
|
||||
-- ^ Error decoding the content
|
||||
| RowErrorSize !Int !Int
|
||||
-- ^ Wrong number of cells in the row
|
||||
| RowErrorHeaders !(Vector (Vector CellError)) !(Vector Text) !(Vector Int)
|
||||
-- ^ Three parts:
|
||||
-- (a) Multiple header cells matched the same expected cell,
|
||||
-- (b) Headers that were missing,
|
||||
-- (c) Missing headers that were lambdas. They cannot be
|
||||
-- shown so instead their positions in the 'Siphon' are given.
|
||||
| RowErrorHeaderSize !Int !Int
|
||||
-- ^ Not enough cells in header, expected, actual
|
||||
| RowErrorMalformed !Int
|
||||
-- ^ Error decoding unicode content, column number
|
||||
deriving (Show,Read,Eq)
|
||||
|
||||
-- data WithEnd c = WithEnd
|
||||
-- { withEndEnded :: !Bool
|
||||
-- , withEndContent :: !c
|
||||
-- }
|
||||
-- | This just actually a specialization of the free applicative.
|
||||
-- Check out @Control.Applicative.Free@ in the @free@ library to
|
||||
-- learn more about this. The meanings of the fields are documented
|
||||
-- slightly more in the source code. Unfortunately, haddock does not
|
||||
-- play nicely with GADTs.
|
||||
data Siphon f c a where
|
||||
SiphonPure ::
|
||||
!a -- function
|
||||
-> Siphon f c a
|
||||
SiphonAp ::
|
||||
!(f c) -- header
|
||||
-> !(c -> Maybe a) -- decoding function
|
||||
-> !(Siphon f c (a -> b)) -- next decoding
|
||||
-> Siphon f c b
|
||||
|
||||
-- data SiphonDecodingError
|
||||
-- { clarify
|
||||
-- }
|
||||
instance Functor (Siphon f c) where
|
||||
fmap f (SiphonPure a) = SiphonPure (f a)
|
||||
fmap f (SiphonAp h c apNext) = SiphonAp h c ((f .) <$> apNext)
|
||||
|
||||
instance Applicative (Siphon f c) where
|
||||
pure = SiphonPure
|
||||
SiphonPure f <*> y = fmap f y
|
||||
SiphonAp h c y <*> z = SiphonAp h c (flip <$> y <*> z)
|
||||
|
||||
|
||||
8
siphon/test/Doctest.hs
Normal file
8
siphon/test/Doctest.hs
Normal file
@ -0,0 +1,8 @@
|
||||
import Test.DocTest
|
||||
|
||||
main :: IO ()
|
||||
main = doctest
|
||||
[ "-isrc"
|
||||
, "src/Siphon.hs"
|
||||
]
|
||||
|
||||
@ -1,38 +1,42 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
|
||||
module Main (main) where
|
||||
|
||||
import Test.QuickCheck (Gen, Arbitrary(..), choose, elements)
|
||||
import Test.HUnit (Assertion,(@?=))
|
||||
import Test.Framework (defaultMain, testGroup, Test)
|
||||
import Test.QuickCheck (Gen, Arbitrary(..), choose, elements, Property)
|
||||
import Test.QuickCheck.Property (Result, succeeded, exception)
|
||||
import Test.HUnit (Assertion,(@?=))
|
||||
import Test.Framework (defaultMain, testGroup, Test)
|
||||
import Test.Framework.Providers.QuickCheck2 (testProperty)
|
||||
import Test.Framework.Providers.HUnit (testCase)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics (Generic)
|
||||
import Test.Framework.Providers.HUnit (testCase)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics (Generic)
|
||||
import Data.Either.Combinators
|
||||
import Colonnade.Types
|
||||
import Siphon.Types
|
||||
import Data.Functor.Identity
|
||||
import Data.Functor.Contravariant (contramap)
|
||||
import Data.Functor.Contravariant.Divisible (divided,conquered)
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.ByteString.Builder as Builder
|
||||
import qualified Data.ByteString.Lazy as LByteString
|
||||
import qualified Data.ByteString as ByteString
|
||||
import qualified Data.ByteString.Char8 as BC8
|
||||
import qualified Colonnade.Decoding as Decoding
|
||||
import qualified Colonnade.Encoding as Encoding
|
||||
import qualified Colonnade.Decoding.ByteString.Char8 as CDB
|
||||
import qualified Colonnade.Encoding.ByteString.Char8 as CEB
|
||||
import qualified Colonnade.Decoding.Text as CDT
|
||||
import qualified Colonnade.Encoding.Text as CET
|
||||
import qualified Siphon.Encoding as SE
|
||||
import qualified Siphon.Decoding as SD
|
||||
import qualified Siphon.Content as SC
|
||||
import qualified Pipes.Prelude as Pipes
|
||||
import Pipes
|
||||
import Colonnade (headed,headless,Colonnade,Headed,Headless)
|
||||
import Data.Profunctor (lmap)
|
||||
import Streaming (Stream,Of(..))
|
||||
import Control.Exception
|
||||
import Debug.Trace
|
||||
import Data.Word (Word8)
|
||||
import Data.Char (ord)
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.ByteString.Builder as Builder
|
||||
import qualified Data.ByteString.Lazy as LByteString
|
||||
import qualified Data.ByteString as ByteString
|
||||
import qualified Data.ByteString.Char8 as BC8
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Colonnade as Colonnade
|
||||
import qualified Siphon as S
|
||||
import qualified Streaming.Prelude as SMP
|
||||
import qualified Data.Text.Lazy as LText
|
||||
import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
import qualified Data.Text.Lazy.Builder.Int as TBuilder
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMain tests
|
||||
@ -40,63 +44,80 @@ main = defaultMain tests
|
||||
tests :: [Test]
|
||||
tests =
|
||||
[ testGroup "ByteString encode/decode"
|
||||
[ testCase "Headless Encoding (int,char,bool)"
|
||||
$ runTestScenario
|
||||
SC.byteStringChar8
|
||||
SE.pipe
|
||||
encodingA
|
||||
"4,c,false\n"
|
||||
, testProperty "Headless Isomorphism (int,char,bool)"
|
||||
$ propIsoPipe $
|
||||
(SE.pipe SC.byteStringChar8 encodingA)
|
||||
>->
|
||||
(void $ SD.headlessPipe SC.byteStringChar8 decodingA)
|
||||
, testCase "Headed Encoding (int,char,bool)"
|
||||
$ runTestScenario
|
||||
SC.byteStringChar8
|
||||
SE.headedPipe
|
||||
[ testCase "Headed Encoding (int,char,bool)"
|
||||
$ runTestScenario [(4,intToWord8 (ord 'c'),False)]
|
||||
S.encodeCsvStreamUtf8
|
||||
encodingB
|
||||
$ ByteString.concat
|
||||
[ "number,letter,boolean\n"
|
||||
, "4,c,false\n"
|
||||
]
|
||||
, testCase "Headed Encoding (int,char,bool) monoidal building"
|
||||
$ runTestScenario
|
||||
SC.byteStringChar8
|
||||
SE.headedPipe
|
||||
$ runTestScenario [(4,'c',False)]
|
||||
S.encodeCsvStreamUtf8
|
||||
encodingC
|
||||
$ ByteString.concat
|
||||
[ "boolean,letter\n"
|
||||
, "false,c\n"
|
||||
]
|
||||
, testCase "Headed Encoding (escaped characters)"
|
||||
$ runTestScenario ["bob","there,be,commas","the \" quote"]
|
||||
S.encodeCsvStreamUtf8
|
||||
encodingF
|
||||
$ ByteString.concat
|
||||
[ "name\n"
|
||||
, "bob\n"
|
||||
, "\"there,be,commas\"\n"
|
||||
, "\"the \"\" quote\"\n"
|
||||
]
|
||||
, testCase "Headed Decoding (int,char,bool)"
|
||||
$ ( runIdentity . SMP.toList )
|
||||
( S.decodeCsvUtf8 decodingB
|
||||
( mapM_ (SMP.yield . BC8.singleton) $ concat
|
||||
[ "number,letter,boolean\n"
|
||||
, "244,z,true\n"
|
||||
]
|
||||
)
|
||||
) @?= ([(244,intToWord8 (ord 'z'),True)] :> Nothing)
|
||||
, testCase "Headed Decoding (geolite)"
|
||||
$ ( runIdentity . SMP.toList )
|
||||
( S.decodeCsvUtf8 decodingGeolite
|
||||
( SMP.yield $ BC8.pack $ concat
|
||||
[ "network,autonomous_system_number,autonomous_system_organization\n"
|
||||
, "1,z,y\n"
|
||||
]
|
||||
)
|
||||
) @?= ([(1,intToWord8 (ord 'z'),intToWord8 (ord 'y'))] :> Nothing)
|
||||
, testCase "Headed Decoding (escaped characters, one big chunk)"
|
||||
$ ( runIdentity . SMP.toList )
|
||||
( S.decodeCsvUtf8 decodingF
|
||||
( SMP.yield $ BC8.pack $ concat
|
||||
[ "name\n"
|
||||
, "drew\n"
|
||||
, "\"martin, drew\"\n"
|
||||
]
|
||||
)
|
||||
) @?= (["drew","martin, drew"] :> Nothing)
|
||||
, testCase "Headed Decoding (escaped characters, character per chunk)"
|
||||
$ ( runIdentity . SMP.toList )
|
||||
( S.decodeCsvUtf8 decodingF
|
||||
( mapM_ (SMP.yield . BC8.singleton) $ concat
|
||||
[ "name\n"
|
||||
, "drew\n"
|
||||
, "\"martin, drew\"\n"
|
||||
]
|
||||
)
|
||||
) @?= (["drew","martin, drew"] :> Nothing)
|
||||
, testProperty "Headed Isomorphism (int,char,bool)"
|
||||
$ propIsoPipe $
|
||||
(SE.headedPipe SC.byteStringChar8 encodingB)
|
||||
>->
|
||||
(void $ SD.headedPipe SC.byteStringChar8 decodingB)
|
||||
]
|
||||
, testGroup "Text encode/decode"
|
||||
[ testCase "Headless Encoding (int,char,bool)"
|
||||
$ runTestScenario
|
||||
SC.text
|
||||
SE.pipe
|
||||
encodingW
|
||||
"4,c,false\n"
|
||||
, testCase "Headless Encoding (Foo,Foo,Foo)"
|
||||
$ runCustomTestScenario
|
||||
SC.text
|
||||
SE.pipe
|
||||
encodingY
|
||||
(FooA,FooA,FooC)
|
||||
"Simple,Simple,\"More\"\"Escaped,\"\"\"\"Chars\"\n"
|
||||
, testProperty "Headless Isomorphism (Foo,Foo,Foo)"
|
||||
$ propIsoPipe $
|
||||
(SE.pipe SC.text encodingY)
|
||||
>->
|
||||
(void $ SD.headlessPipe SC.text decodingY)
|
||||
$ propIsoStream BC8.unpack
|
||||
(S.decodeCsvUtf8 decodingB)
|
||||
(S.encodeCsvStreamUtf8 encodingB)
|
||||
]
|
||||
]
|
||||
|
||||
intToWord8 :: Int -> Word8
|
||||
intToWord8 = fromIntegral
|
||||
|
||||
data Foo = FooA | FooB | FooC
|
||||
deriving (Generic,Eq,Ord,Show,Read,Bounded,Enum)
|
||||
|
||||
@ -112,96 +133,128 @@ fooToString x = case x of
|
||||
encodeFoo :: (String -> c) -> Foo -> c
|
||||
encodeFoo f = f . fooToString
|
||||
|
||||
fooFromString :: String -> Either String Foo
|
||||
fooFromString :: String -> Maybe Foo
|
||||
fooFromString x = case x of
|
||||
"Simple" -> Right FooA
|
||||
"With,Escaped\nChars" -> Right FooB
|
||||
"More\"Escaped,\"\"Chars" -> Right FooC
|
||||
_ -> Left "failed to decode Foo"
|
||||
"Simple" -> Just FooA
|
||||
"With,Escaped\nChars" -> Just FooB
|
||||
"More\"Escaped,\"\"Chars" -> Just FooC
|
||||
_ -> Nothing
|
||||
|
||||
decodeFoo :: (c -> String) -> c -> Either String Foo
|
||||
decodeFoo :: (c -> String) -> c -> Maybe Foo
|
||||
decodeFoo f = fooFromString . f
|
||||
|
||||
decodingA :: Decoding Headless ByteString (Int,Char,Bool)
|
||||
decodingA :: Siphon Headless ByteString (Int,Char,Bool)
|
||||
decodingA = (,,)
|
||||
<$> Decoding.headless CDB.int
|
||||
<*> Decoding.headless CDB.char
|
||||
<*> Decoding.headless CDB.bool
|
||||
<$> S.headless dbInt
|
||||
<*> S.headless dbChar
|
||||
<*> S.headless dbBool
|
||||
|
||||
decodingB :: Decoding Headed ByteString (Int,Char,Bool)
|
||||
decodingB :: Siphon Headed ByteString (Int,Word8,Bool)
|
||||
decodingB = (,,)
|
||||
<$> Decoding.headed "number" CDB.int
|
||||
<*> Decoding.headed "letter" CDB.char
|
||||
<*> Decoding.headed "boolean" CDB.bool
|
||||
<$> S.headed "number" dbInt
|
||||
<*> S.headed "letter" dbWord8
|
||||
<*> S.headed "boolean" dbBool
|
||||
|
||||
encodingA :: Encoding Headless ByteString (Int,Char,Bool)
|
||||
encodingA = contramap tripleToPairs
|
||||
$ divided (Encoding.headless CEB.int)
|
||||
$ divided (Encoding.headless CEB.char)
|
||||
$ divided (Encoding.headless CEB.bool)
|
||||
$ conquered
|
||||
decodingF :: Siphon Headed ByteString ByteString
|
||||
decodingF = S.headed "name" Just
|
||||
|
||||
encodingW :: Encoding Headless Text (Int,Char,Bool)
|
||||
encodingW = contramap tripleToPairs
|
||||
$ divided (Encoding.headless CET.int)
|
||||
$ divided (Encoding.headless CET.char)
|
||||
$ divided (Encoding.headless CET.bool)
|
||||
$ conquered
|
||||
decodingGeolite :: Siphon Headed ByteString (Int,Word8,Word8)
|
||||
decodingGeolite = (,,)
|
||||
<$> S.headed "network" dbInt
|
||||
<*> S.headed "autonomous_system_number" dbWord8
|
||||
<*> S.headed "autonomous_system_organization" dbWord8
|
||||
|
||||
encodingY :: Encoding Headless Text (Foo,Foo,Foo)
|
||||
encodingY = contramap tripleToPairs
|
||||
$ divided (Encoding.headless $ encodeFoo Text.pack)
|
||||
$ divided (Encoding.headless $ encodeFoo Text.pack)
|
||||
$ divided (Encoding.headless $ encodeFoo Text.pack)
|
||||
$ conquered
|
||||
|
||||
decodingY :: Decoding Headless Text (Foo,Foo,Foo)
|
||||
encodingA :: Colonnade Headless (Int,Char,Bool) ByteString
|
||||
encodingA = mconcat
|
||||
[ lmap fst3 (headless ebInt)
|
||||
, lmap snd3 (headless ebChar)
|
||||
, lmap thd3 (headless ebBool)
|
||||
]
|
||||
|
||||
encodingW :: Colonnade Headless (Int,Char,Bool) Text
|
||||
encodingW = mconcat
|
||||
[ lmap fst3 (headless etInt)
|
||||
, lmap snd3 (headless etChar)
|
||||
, lmap thd3 (headless etBool)
|
||||
]
|
||||
|
||||
encodingY :: Colonnade Headless (Foo,Foo,Foo) Text
|
||||
encodingY = mconcat
|
||||
[ lmap fst3 (headless $ encodeFoo Text.pack)
|
||||
, lmap snd3 (headless $ encodeFoo Text.pack)
|
||||
, lmap thd3 (headless $ encodeFoo Text.pack)
|
||||
]
|
||||
|
||||
decodingY :: Siphon Headless Text (Foo,Foo,Foo)
|
||||
decodingY = (,,)
|
||||
<$> Decoding.headless (decodeFoo Text.unpack)
|
||||
<*> Decoding.headless (decodeFoo Text.unpack)
|
||||
<*> Decoding.headless (decodeFoo Text.unpack)
|
||||
<$> S.headless (decodeFoo Text.unpack)
|
||||
<*> S.headless (decodeFoo Text.unpack)
|
||||
<*> S.headless (decodeFoo Text.unpack)
|
||||
|
||||
encodingB :: Encoding Headed ByteString (Int,Char,Bool)
|
||||
encodingB = contramap tripleToPairs
|
||||
$ divided (Encoding.headed "number" CEB.int)
|
||||
$ divided (Encoding.headed "letter" CEB.char)
|
||||
$ divided (Encoding.headed "boolean" CEB.bool)
|
||||
$ conquered
|
||||
encodingF :: Colonnade Headed ByteString ByteString
|
||||
encodingF = headed "name" id
|
||||
|
||||
encodingC :: Encoding Headed ByteString (Int,Char,Bool)
|
||||
encodingB :: Colonnade Headed (Int,Word8,Bool) ByteString
|
||||
encodingB = mconcat
|
||||
[ lmap fst3 (headed "number" ebInt)
|
||||
, lmap snd3 (headed "letter" ebWord8)
|
||||
, lmap thd3 (headed "boolean" ebBool)
|
||||
]
|
||||
|
||||
encodingC :: Colonnade Headed (Int,Char,Bool) ByteString
|
||||
encodingC = mconcat
|
||||
[ contramap thd3 $ Encoding.headed "boolean" CEB.bool
|
||||
, contramap snd3 $ Encoding.headed "letter" CEB.char
|
||||
[ lmap thd3 $ headed "boolean" ebBool
|
||||
, lmap snd3 $ headed "letter" ebChar
|
||||
]
|
||||
|
||||
tripleToPairs :: (a,b,c) -> (a,(b,(c,())))
|
||||
tripleToPairs (a,b,c) = (a,(b,(c,())))
|
||||
|
||||
propIsoPipe :: Eq a => Pipe a a Identity () -> [a] -> Bool
|
||||
propIsoPipe p as = (Pipes.toList $ each as >-> p) == as
|
||||
propIsoStream :: (Eq a, Show a, Monoid c)
|
||||
=> (c -> String)
|
||||
-> (Stream (Of c) Identity () -> Stream (Of a) Identity (Maybe SiphonError))
|
||||
-> (Stream (Of a) Identity () -> Stream (Of c) Identity ())
|
||||
-> [a]
|
||||
-> Result
|
||||
propIsoStream toStr decode encode as =
|
||||
let asNew :> m = runIdentity $ SMP.toList $ decode $ encode $ SMP.each as
|
||||
in case m of
|
||||
Nothing -> if as == asNew
|
||||
then succeeded
|
||||
else exception ("expected " ++ show as ++ " but got " ++ show asNew) myException
|
||||
Just err ->
|
||||
let csv = toStr $ mconcat $ runIdentity $ SMP.toList_ $ encode $ SMP.each as
|
||||
in exception (S.humanizeSiphonError err ++ "\nGenerated CSV\n" ++ csv) myException
|
||||
|
||||
runTestScenario :: (Monoid c, Eq c, Show c)
|
||||
=> Siphon c
|
||||
-> (Siphon c -> Encoding f c (Int,Char,Bool) -> Pipe (Int,Char,Bool) c Identity ())
|
||||
-> Encoding f c (Int,Char,Bool)
|
||||
data MyException = MyException
|
||||
deriving (Show,Read,Eq)
|
||||
instance Exception MyException
|
||||
|
||||
myException :: SomeException
|
||||
myException = SomeException MyException
|
||||
|
||||
runTestScenario :: (Monoid c, Eq c, Show c, Eq a, Show a)
|
||||
=> [a]
|
||||
-> (Colonnade f a c -> Stream (Of a) Identity () -> Stream (Of c) Identity ())
|
||||
-> Colonnade f a c
|
||||
-> c
|
||||
-> Assertion
|
||||
runTestScenario s p e c =
|
||||
( mconcat $ Pipes.toList $
|
||||
Pipes.yield (4,'c',False) >-> p s e
|
||||
runTestScenario as p e c =
|
||||
( mconcat (runIdentity (SMP.toList_ (p e (mapM_ SMP.yield as))))
|
||||
) @?= c
|
||||
|
||||
runCustomTestScenario :: (Monoid c, Eq c, Show c)
|
||||
=> Siphon c
|
||||
-> (Siphon c -> Encoding f c a -> Pipe a c Identity ())
|
||||
-> Encoding f c a
|
||||
-> a
|
||||
-> c
|
||||
-> Assertion
|
||||
runCustomTestScenario s p e a c =
|
||||
( mconcat $ Pipes.toList $
|
||||
Pipes.yield a >-> p s e
|
||||
) @?= c
|
||||
-- runCustomTestScenario :: (Monoid c, Eq c, Show c)
|
||||
-- => Siphon c
|
||||
-- -> (Siphon c -> Colonnade f a c -> Pipe a c Identity ())
|
||||
-- -> Colonnade f a c
|
||||
-- -> a
|
||||
-- -> c
|
||||
-- -> Assertion
|
||||
-- runCustomTestScenario s p e a c =
|
||||
-- ( mconcat $ Pipes.toList $
|
||||
-- Pipes.yield a >-> p s e
|
||||
-- ) @?= c
|
||||
|
||||
-- testEncodingA :: Assertion
|
||||
-- testEncodingA = runTestScenario encodingA "4,c,false\n"
|
||||
@ -225,3 +278,63 @@ snd3 (a,b,c) = b
|
||||
thd3 :: (a,b,c) -> c
|
||||
thd3 (a,b,c) = c
|
||||
|
||||
|
||||
dbChar :: ByteString -> Maybe Char
|
||||
dbChar b = case BC8.length b of
|
||||
1 -> Just (BC8.head b)
|
||||
_ -> Nothing
|
||||
|
||||
dbWord8 :: ByteString -> Maybe Word8
|
||||
dbWord8 b = case B.length b of
|
||||
1 -> Just (B.head b)
|
||||
_ -> Nothing
|
||||
|
||||
dbInt :: ByteString -> Maybe Int
|
||||
dbInt b = do
|
||||
(a,bsRem) <- BC8.readInt b
|
||||
if ByteString.null bsRem
|
||||
then Just a
|
||||
else Nothing
|
||||
|
||||
dbBool :: ByteString -> Maybe Bool
|
||||
dbBool b
|
||||
| b == BC8.pack "true" = Just True
|
||||
| b == BC8.pack "false" = Just False
|
||||
| otherwise = Nothing
|
||||
|
||||
ebChar :: Char -> ByteString
|
||||
ebChar = BC8.singleton
|
||||
|
||||
ebWord8 :: Word8 -> ByteString
|
||||
ebWord8 = B.singleton
|
||||
|
||||
ebInt :: Int -> ByteString
|
||||
ebInt = LByteString.toStrict
|
||||
. Builder.toLazyByteString
|
||||
. Builder.intDec
|
||||
|
||||
ebBool :: Bool -> ByteString
|
||||
ebBool x = case x of
|
||||
True -> BC8.pack "true"
|
||||
False -> BC8.pack "false"
|
||||
|
||||
ebByteString :: ByteString -> ByteString
|
||||
ebByteString = id
|
||||
|
||||
|
||||
etChar :: Char -> Text
|
||||
etChar = Text.singleton
|
||||
|
||||
etInt :: Int -> Text
|
||||
etInt = LText.toStrict
|
||||
. TBuilder.toLazyText
|
||||
. TBuilder.decimal
|
||||
|
||||
etText :: Text -> Text
|
||||
etText = id
|
||||
|
||||
etBool :: Bool -> Text
|
||||
etBool x = case x of
|
||||
True -> Text.pack "true"
|
||||
False -> Text.pack "false"
|
||||
|
||||
|
||||
26
stack-haddock-upload
Executable file
26
stack-haddock-upload
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Author: Dimitri Sabadie <dimitri.sabadie@gmail.com>
|
||||
# 2015
|
||||
|
||||
dist=`stack path --dist-dir --stack-yaml ./stack.yaml 2> /dev/null`
|
||||
|
||||
echo -e "\033[1;36mGenerating documentation...\033[0m"
|
||||
stack haddock 2> /dev/null
|
||||
|
||||
if [ "$?" -eq "0" ]; then
|
||||
docdir=$dist/doc/html
|
||||
cd $docdir
|
||||
doc=$1-$2-docs
|
||||
echo -e "Compressing documentation from \033[1;34m$docdir\033[0m for \033[1;35m$1\033[0m-\033[1;33m$2\033[1;30m"
|
||||
cp -r $1 $doc
|
||||
tar -c -v -z --format=ustar -f $doc.tar.gz $doc
|
||||
echo -e "\033[1;32mUploading to Hackage...\033[0m"
|
||||
read -p "Hackage username: " username
|
||||
read -p "Hackage password: " -s password
|
||||
echo ""
|
||||
curl -X PUT -H 'Content-Type: application/x-tar' -H 'Content-Encoding: gzip' --data-binary "@$doc.tar.gz" "https://$username:$password@hackage.haskell.org/package/$1-$2/docs"
|
||||
exit $?
|
||||
else
|
||||
echo -e "\033[1;31mNot in a stack-powered project\033[0m"
|
||||
fi
|
||||
54
stack.yaml
54
stack.yaml
@ -1,56 +1,14 @@
|
||||
# This file was automatically generated by 'stack init'
|
||||
#
|
||||
# Some commonly used options have been documented as comments in this file.
|
||||
# For advanced use and comprehensive documentation of the format, please see:
|
||||
# http://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
|
||||
# A snapshot resolver dictates the compiler version and the set of packages
|
||||
# to be used for project dependencies. For example:
|
||||
#
|
||||
# resolver: lts-3.5
|
||||
# resolver: nightly-2015-09-21
|
||||
# resolver: ghc-7.10.2
|
||||
# resolver: ghcjs-0.1.0_ghc-7.10.2
|
||||
# resolver:
|
||||
# name: custom-snapshot
|
||||
# location: "./custom-snapshot.yaml"
|
||||
resolver: lts-8.0
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
#
|
||||
# packages:
|
||||
# - some-directory
|
||||
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
|
||||
# - location:
|
||||
# git: https://github.com/commercialhaskell/stack.git
|
||||
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
# extra-dep: true
|
||||
# subdirs:
|
||||
# - auto-update
|
||||
# - wai
|
||||
#
|
||||
# A package marked 'extra-dep: true' will only be built if demanded by a
|
||||
# non-dependency (i.e. a user package), and its test suites and benchmarks
|
||||
# will not be run. This is useful for tweaking upstream packages.
|
||||
resolver: nightly-2018-06-11
|
||||
packages:
|
||||
- 'colonnade'
|
||||
- 'yesod-colonnade'
|
||||
- 'reflex-dom-colonnade'
|
||||
- 'blaze-colonnade'
|
||||
- 'lucid-colonnade'
|
||||
- 'siphon'
|
||||
- 'geolite-csv'
|
||||
# Dependency packages to be pulled from upstream that are not in the resolver
|
||||
# (e.g., acme-missiles-0.3)
|
||||
- 'yesod-colonnade'
|
||||
# - 'geolite-csv'
|
||||
|
||||
extra-deps:
|
||||
- 'reflex-dom-0.3'
|
||||
- 'ref-tf-0.4'
|
||||
- 'reflex-0.4.0'
|
||||
- 'haskell-src-exts-1.16.0.1'
|
||||
- 'syb-0.5.1'
|
||||
- 'ip-0.8.4'
|
||||
- 'yesod-elements-1.1'
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
flags: {}
|
||||
|
||||
@ -13,27 +13,30 @@ module Yesod.Colonnade
|
||||
, textCell
|
||||
, builderCell
|
||||
, anchorCell
|
||||
, anchorWidget
|
||||
-- * Apply
|
||||
, encodeHeadedWidgetTable
|
||||
, encodeHeadlessWidgetTable
|
||||
, encodeHeadedCellTable
|
||||
, encodeHeadlessCellTable
|
||||
, encodeWidgetTable
|
||||
, encodeCellTable
|
||||
, encodeDefinitionTable
|
||||
, encodeListItems
|
||||
) where
|
||||
|
||||
import Yesod.Core
|
||||
import Yesod.Core.Types (Body(..),GWData(..),WidgetT(..))
|
||||
import Yesod.Core.Types (Body(..),GWData(..),WidgetFor(..),wdRef)
|
||||
import Colonnade (Colonnade,Headed,Headless)
|
||||
import Data.Text (Text)
|
||||
import Control.Monad
|
||||
import Data.IORef (modifyIORef')
|
||||
import Data.Monoid
|
||||
import Data.String (IsString(..))
|
||||
import Text.Blaze (Attribute,toValue)
|
||||
import Data.Foldable
|
||||
import Yesod.Elements (table_,thead_,tbody_,tr_,td_,th_,ul_,li_,a_)
|
||||
import Data.Semigroup (Semigroup)
|
||||
import qualified Data.Semigroup as SG
|
||||
import qualified Text.Blaze.Html5.Attributes as HA
|
||||
import qualified Text.Blaze.Html5 as H
|
||||
import qualified Colonnade.Encode as Encode
|
||||
import qualified Colonnade.Encode as E
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Text.Lazy as LText
|
||||
import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
@ -41,19 +44,21 @@ import qualified Data.Text.Lazy.Builder as TBuilder
|
||||
-- | The attributes that will be applied to a @<td>@ and
|
||||
-- the HTML content that will go inside it.
|
||||
data Cell site = Cell
|
||||
{ cellAttrs :: !Attribute
|
||||
, cellContents :: !(WidgetT site IO ())
|
||||
{ cellAttrs :: [Attribute]
|
||||
, cellContents :: !(WidgetFor site ())
|
||||
}
|
||||
|
||||
instance IsString (Cell site) where
|
||||
fromString = stringCell
|
||||
|
||||
instance Semigroup (Cell site) where
|
||||
Cell a1 c1 <> Cell a2 c2 = Cell (mappend a1 a2) (mappend c1 c2)
|
||||
instance Monoid (Cell site) where
|
||||
mempty = Cell mempty mempty
|
||||
mappend (Cell a1 c1) (Cell a2 c2) = Cell (mappend a1 a2) (mappend c1 c2)
|
||||
mappend = (SG.<>)
|
||||
|
||||
-- | Create a 'Cell' from a 'Widget'
|
||||
cell :: WidgetT site IO () -> Cell site
|
||||
cell :: WidgetFor site () -> Cell site
|
||||
cell = Cell mempty
|
||||
|
||||
-- | Create a 'Cell' from a 'String'
|
||||
@ -68,32 +73,41 @@ textCell = cell . toWidget . toHtml
|
||||
builderCell :: TBuilder.Builder -> Cell site
|
||||
builderCell = cell . toWidget . toHtml . LText.toStrict . TBuilder.toLazyText
|
||||
|
||||
-- | Creata a 'Cell' whose content is hyperlinked by wrapping
|
||||
-- | Create a 'Cell' whose content is hyperlinked by wrapping
|
||||
-- it in an @\<a\>@.
|
||||
anchorCell ::
|
||||
(a -> Route site) -- ^ Route that will go in @href@ attribute
|
||||
-> (a -> WidgetT site IO ()) -- ^ Content wrapped by @<a>@ tag
|
||||
-> (a -> WidgetFor site ()) -- ^ Content wrapped by @<a>@ tag
|
||||
-> a -- ^ Value
|
||||
-> Cell site
|
||||
anchorCell getRoute getContent a = cell $ do
|
||||
anchorCell getRoute getContent = cell . anchorWidget getRoute getContent
|
||||
|
||||
-- | Create a widget whose content is hyperlinked by wrapping
|
||||
-- it in an @\<a\>@.
|
||||
anchorWidget ::
|
||||
(a -> Route site) -- ^ Route that will go in @href@ attribute
|
||||
-> (a -> WidgetFor site ()) -- ^ Content wrapped by @<a>@ tag
|
||||
-> a -- ^ Value
|
||||
-> WidgetFor site ()
|
||||
anchorWidget getRoute getContent a = do
|
||||
urlRender <- getUrlRender
|
||||
a_ (HA.href (toValue (urlRender (getRoute a)))) (getContent a)
|
||||
a_ [HA.href (toValue (urlRender (getRoute a)))] (getContent a)
|
||||
|
||||
-- | This determines the attributes that are added
|
||||
-- to the individual @li@s by concatenating the header\'s
|
||||
-- attributes with the data\'s attributes.
|
||||
encodeListItems ::
|
||||
(WidgetT site IO () -> WidgetT site IO ())
|
||||
(WidgetFor site () -> WidgetFor site ())
|
||||
-- ^ Wrapper for items, often @ul@
|
||||
-> (WidgetT site IO () -> WidgetT site IO () -> WidgetT site IO ())
|
||||
-> (WidgetFor site () -> WidgetFor site () -> WidgetFor site ())
|
||||
-- ^ Combines header with data
|
||||
-> Colonnade Headed (Cell site) a
|
||||
-> Colonnade Headed a (Cell site)
|
||||
-- ^ How to encode data as a row
|
||||
-> a
|
||||
-- ^ The value to display
|
||||
-> WidgetT site IO ()
|
||||
-> WidgetFor site ()
|
||||
encodeListItems ulWrap combine enc =
|
||||
ulWrap . Encode.bothMonadic_ enc
|
||||
ulWrap . E.bothMonadic_ enc
|
||||
(\(Cell ha hc) (Cell ba bc) ->
|
||||
li_ (ha <> ba) (combine hc bc)
|
||||
)
|
||||
@ -102,106 +116,68 @@ encodeListItems ulWrap combine enc =
|
||||
-- first column and the data displayed in the second column. Note
|
||||
-- that the generated HTML table does not have a @thead@.
|
||||
encodeDefinitionTable ::
|
||||
Attribute
|
||||
[Attribute]
|
||||
-- ^ Attributes of @table@ element.
|
||||
-> Colonnade Headed (Cell site) a
|
||||
-> Colonnade Headed a (Cell site)
|
||||
-- ^ How to encode data as a row
|
||||
-> a
|
||||
-- ^ The value to display
|
||||
-> WidgetT site IO ()
|
||||
encodeDefinitionTable attrs enc a = table_ attrs $ tbody_ mempty $
|
||||
Encode.bothMonadic_ enc
|
||||
(\theKey theValue -> tr_ mempty $ do
|
||||
-> WidgetFor site ()
|
||||
encodeDefinitionTable attrs enc a = table_ attrs $ tbody_ [] $
|
||||
E.bothMonadic_ enc
|
||||
(\theKey theValue -> tr_ [] $ do
|
||||
widgetFromCell td_ theKey
|
||||
widgetFromCell td_ theValue
|
||||
) a
|
||||
|
||||
-- | If you are using the bootstrap css framework, then you may want
|
||||
-- | Encode an html table with attributes on the table cells.
|
||||
-- If you are using the bootstrap css framework, then you may want
|
||||
-- to call this with the first argument as:
|
||||
--
|
||||
-- > encodeHeadedCellTable (HA.class_ "table table-striped") ...
|
||||
encodeHeadedCellTable :: Foldable f
|
||||
=> Attribute -- ^ Attributes of @table@ element
|
||||
-> Colonnade Headed (Cell site) a -- ^ How to encode data as a row
|
||||
-- > encodeCellTable (HA.class_ "table table-striped") ...
|
||||
encodeCellTable :: (Foldable f, E.Headedness h)
|
||||
=> [Attribute] -- ^ Attributes of @table@ element
|
||||
-> Colonnade h a (Cell site) -- ^ How to encode data as a row
|
||||
-> f a -- ^ Rows of data
|
||||
-> WidgetT site IO ()
|
||||
encodeHeadedCellTable = encodeTable
|
||||
(Just mempty) mempty (const mempty) widgetFromCell
|
||||
-> WidgetFor site ()
|
||||
encodeCellTable = encodeTable
|
||||
(E.headednessPure mempty) mempty (const mempty) widgetFromCell
|
||||
|
||||
encodeHeadlessCellTable :: Foldable f
|
||||
=> Attribute -- ^ Attributes of @table@ element
|
||||
-> Colonnade Headless (Cell site) a -- ^ How to encode data as columns
|
||||
-- | Encode an html table.
|
||||
encodeWidgetTable :: (Foldable f, E.Headedness h)
|
||||
=> [Attribute] -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade h a (WidgetFor site ()) -- ^ How to encode data as columns
|
||||
-> f a -- ^ Rows of data
|
||||
-> WidgetT site IO ()
|
||||
encodeHeadlessCellTable = encodeTable
|
||||
Nothing mempty (const mempty) widgetFromCell
|
||||
|
||||
encodeHeadedWidgetTable :: Foldable f
|
||||
=> Attribute -- ^ Attributes of @table@ element
|
||||
-> Colonnade Headed (WidgetT site IO ()) a -- ^ How to encode data as columns
|
||||
-> f a -- ^ Rows of data
|
||||
-> WidgetT site IO ()
|
||||
encodeHeadedWidgetTable = encodeTable
|
||||
(Just mempty) mempty (const mempty) ($ mempty)
|
||||
|
||||
encodeHeadlessWidgetTable :: Foldable f
|
||||
=> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade Headless (WidgetT site IO ()) a -- ^ How to encode data as columns
|
||||
-> f a -- ^ Rows of data
|
||||
-> WidgetT site IO ()
|
||||
encodeHeadlessWidgetTable = encodeTable
|
||||
Nothing mempty (const mempty) ($ mempty)
|
||||
-> WidgetFor site ()
|
||||
encodeWidgetTable = encodeTable
|
||||
(E.headednessPure mempty) mempty (const mempty) ($ mempty)
|
||||
|
||||
-- | Encode a table. This handles a very general case and
|
||||
-- is seldom needed by users. One of the arguments provided is
|
||||
-- used to add attributes to the generated @\<tr\>@ elements.
|
||||
encodeTable ::
|
||||
(Foldable f, Foldable h)
|
||||
=> Maybe Attribute -- ^ Attributes of @\<thead\>@, pass 'Nothing' to omit @\<thead\>@
|
||||
-> Attribute -- ^ Attributes of @\<tbody\>@ element
|
||||
-> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element
|
||||
-> ((Attribute -> WidgetT site IO () -> WidgetT site IO ()) -> c -> WidgetT site IO ()) -- ^ Wrap content and convert to 'Html'
|
||||
-> Attribute -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade h c a -- ^ How to encode data as a row
|
||||
(Foldable f, E.Headedness h)
|
||||
=> h [Attribute] -- ^ Attributes of @\<thead\>@
|
||||
-> [Attribute] -- ^ Attributes of @\<tbody\>@ element
|
||||
-> (a -> [Attribute]) -- ^ Attributes of each @\<tr\>@ element
|
||||
-> (([Attribute] -> WidgetFor site () -> WidgetFor site ()) -> c -> WidgetFor site ()) -- ^ Wrap content and convert to 'Html'
|
||||
-> [Attribute] -- ^ Attributes of @\<table\>@ element
|
||||
-> Colonnade h a c -- ^ How to encode data as a row
|
||||
-> f a -- ^ Collection of data
|
||||
-> WidgetT site IO ()
|
||||
encodeTable mtheadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
|
||||
-> WidgetFor site ()
|
||||
encodeTable theadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
|
||||
table_ tableAttrs $ do
|
||||
for_ mtheadAttrs $ \theadAttrs -> do
|
||||
thead_ theadAttrs $ do
|
||||
Encode.headerMonadicGeneral_ colonnade (wrapContent th_)
|
||||
for_ E.headednessExtract $ \unhead ->
|
||||
thead_ (unhead theadAttrs) $ do
|
||||
E.headerMonadicGeneral_ colonnade (wrapContent th_)
|
||||
tbody_ tbodyAttrs $ do
|
||||
forM_ xs $ \x -> do
|
||||
tr_ (trAttrs x) (Encode.rowMonadic_ colonnade (wrapContent td_) x)
|
||||
tr_ (trAttrs x) (E.rowMonadic_ colonnade (wrapContent td_) x)
|
||||
|
||||
widgetFromCell ::
|
||||
(Attribute -> WidgetT site IO () -> WidgetT site IO ())
|
||||
([Attribute] -> WidgetFor site () -> WidgetFor site ())
|
||||
-> Cell site
|
||||
-> WidgetT site IO ()
|
||||
-> WidgetFor site ()
|
||||
widgetFromCell f (Cell attrs contents) =
|
||||
f attrs contents
|
||||
|
||||
tr_,tbody_,thead_,table_,td_,th_,ul_,li_,a_ ::
|
||||
Attribute -> WidgetT site IO () -> WidgetT site IO ()
|
||||
|
||||
table_ = liftParent H.table
|
||||
thead_ = liftParent H.thead
|
||||
tbody_ = liftParent H.tbody
|
||||
tr_ = liftParent H.tr
|
||||
td_ = liftParent H.td
|
||||
th_ = liftParent H.th
|
||||
ul_ = liftParent H.ul
|
||||
li_ = liftParent H.li
|
||||
a_ = liftParent H.a
|
||||
|
||||
liftParent :: (Html -> Html) -> Attribute -> WidgetT site IO a -> WidgetT site IO a
|
||||
liftParent el attrs (WidgetT f) = WidgetT $ \hdata -> do
|
||||
(a,gwd) <- f hdata
|
||||
let Body bodyFunc = gwdBody gwd
|
||||
newBodyFunc render =
|
||||
el H.! attrs $ (bodyFunc render)
|
||||
return (a,gwd { gwdBody = Body newBodyFunc })
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,30 +1,33 @@
|
||||
name: yesod-colonnade
|
||||
version: 0.4
|
||||
synopsis: Helper functions for using yesod with colonnade
|
||||
description: Yesod and colonnade
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2016 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
cabal-version: 2.0
|
||||
name: yesod-colonnade
|
||||
version: 1.3.0.2
|
||||
synopsis: Helper functions for using yesod with colonnade
|
||||
description: Yesod and colonnade
|
||||
homepage: https://github.com/andrewthad/colonnade#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Andrew Martin
|
||||
maintainer: andrew.thaddeus@gmail.com
|
||||
copyright: 2018 Andrew Martin
|
||||
category: web
|
||||
build-type: Simple
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules:
|
||||
Yesod.Colonnade
|
||||
build-depends:
|
||||
base >= 4.7 && < 5
|
||||
, colonnade >= 1.0 && < 1.1
|
||||
, yesod-core >= 1.4 && < 1.5
|
||||
base >= 4.9.1 && < 4.14
|
||||
, colonnade >= 1.2 && < 1.3
|
||||
, yesod-core >= 1.6 && < 1.7
|
||||
, conduit >= 1.3 && < 1.4
|
||||
, conduit-extra >= 1.3 && < 1.4
|
||||
, text >= 1.0 && < 1.3
|
||||
, blaze-markup >= 0.7 && < 0.9
|
||||
, blaze-html >= 0.8 && < 0.10
|
||||
, yesod-elements >= 1.1 && < 1.2
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
type: git
|
||||
location: https://github.com/andrewthad/colonnade
|
||||
|
||||
Loading…
Reference in New Issue
Block a user