Prepare 1.2.0.3 release.

Removed other subprojects.
Reformatted.
Use new .github workflows.
Updated package metadata.
This commit is contained in:
Brian McKeon 2024-03-06 21:23:42 -05:00
parent 22dfe8330f
commit f3db03012d
60 changed files with 1377 additions and 5191 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
@byteverse/l3c

11
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,11 @@
name: build
on:
pull_request:
branches:
- "*"
jobs:
call-workflow:
uses: byteverse/.github/.github/workflows/build-matrix.yaml@main
with:
cabal-file: colonnade.cabal

10
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,10 @@
name: release
on:
push:
tags:
- "*"
jobs:
call-workflow:
uses: byteverse/.github/.github/workflows/release.yaml@main
secrets: inherit

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.vscode/
*.aux
cabal-dev
.cabal-sandbox

5
CHANGELOG.md Normal file
View File

@ -0,0 +1,5 @@
# Revision history for colonnade
## 1.2.0.3 -- 2024-03-06
* Update package metadata.

View File

@ -1,11 +1,9 @@
Most of the tests use doctest, which isn't run like a normal test suite (I guess).
To run doctests:
To run these tests, first make sure `doctest` is on the `PATH` (i.e. `cabal install doctest`), then run the following commands:
First make sure `doctest` is on the `PATH` (i.e. `cabal install doctest`).
Then run:
```
cabal repl --build-depends=QuickCheck --with-ghc=doctest --repl-options="-fno-warn-orphans" siphon
cabal repl --build-depends=QuickCheck --with-ghc=doctest --repl-options="-fno-warn-orphans" colonnade
cabal repl --build-depends=QuickCheck --with-ghc=doctest --repl-options="-fno-warn-orphans" blaze-colonnade
cabal repl --with-ghc=doctest --repl-options="-fno-warn-orphans -Wno-x-partial" colonnade
```
There are no tests for lucid-colonnade at present.

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,38 +0,0 @@
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.8 && < 5
, colonnade >= 1.1 && < 1.3
, blaze-markup >= 0.7 && < 0.9
, blaze-html >= 0.8 && < 0.10
, profunctors >= 5.0 && < 5.7
, text >= 1.2 && < 2.1
default-language: Haskell2010
source-repository head
type: git
location: https://github.com/andrewthad/colonnade

View File

@ -1,48 +0,0 @@
#!/bin/bash
set -e
if [ "$#" -ne 1 ]; then
echo "Usage: scripts/hackage-docs.sh HACKAGE_USER"
exit 1
fi
user=$1
cabal_file=$(find . -maxdepth 1 -name "*.cabal" -print -quit)
if [ ! -f "$cabal_file" ]; then
echo "Run this script in the top-level package directory"
exit 1
fi
pkg=$(awk -F ":[[:space:]]*" 'tolower($1)=="name" { print $2 }' < "$cabal_file")
ver=$(awk -F ":[[:space:]]*" 'tolower($1)=="version" { print $2 }' < "$cabal_file")
if [ -z "$pkg" ]; then
echo "Unable to determine package name"
exit 1
fi
if [ -z "$ver" ]; then
echo "Unable to determine package version"
exit 1
fi
echo "Detected package: $pkg-$ver"
dir=$(mktemp -d build-docs.XXXXXX)
trap 'rm -r "$dir"' EXIT
# cabal haddock --hoogle --hyperlink-source --html-location='/package/$pkg-$version/docs' --contents-location='/package/$pkg-$version'
stack haddock
cp -R .stack-work/dist/x86_64-linux/Cabal-1.22.5.0/doc/html/$pkg/ $dir/$pkg-$ver-docs
# /home/andrew/.stack/snapshots/x86_64-linux/lts-5.17/7.10.3/doc/index.html
tar cvz -C $dir --format=ustar -f $dir/$pkg-$ver-docs.tar.gz $pkg-$ver-docs
curl -X PUT \
-H 'Content-Type: application/x-tar' \
-H 'Content-Encoding: gzip' \
-u "$user" \
--data-binary "@$dir/$pkg-$ver-docs.tar.gz" \
"https://hackage.haskell.org/package/$pkg-$ver/docs"

View File

@ -1,549 +0,0 @@
{-# 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
-- recommended that users read the documentation for @colonnade@ first,
-- since this library builds on the abstractions introduced there.
-- A concise example of this library\'s use:
--
-- >>> :set -XOverloadedStrings
-- >>> :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 (encodeHtmlTable mempty col rows)
-- <table>
-- <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>
-- <tr><td>70-79</td><td>C</td></tr>
-- </tbody>
-- </table>
module Text.Blaze.Colonnade
( -- * Apply
encodeHtmlTable
, encodeCellTable
, encodeTable
, encodeCappedTable
-- * Cell
-- $build
, Cell(..)
, htmlCell
, stringCell
, textCell
, lazyTextCell
, builderCell
, htmlFromCell
-- * Interactive
, printCompactHtml
, printVeryCompactHtml
-- * Tutorial
-- $setup
-- * Discussion
-- $discussion
) where
import Text.Blaze (Attribute,(!))
import Text.Blaze.Html (Html, toHtml)
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 qualified Data.List as List
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 E
import qualified Data.Text as Text
import qualified Data.Text.Lazy as LText
import qualified Data.Text.Lazy.Builder as TBuilder
-- $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.Profunctor (Profunctor(lmap))
-- >>> import Colonnade (Colonnade,Headed,Headless,headed,cap,Fascia(..))
-- >>> import Text.Blaze.Html (Html, toHtml, toValue)
-- >>> import qualified Text.Blaze.Html5 as H
-- >>> data Department = Management | Sales | Engineering deriving (Show,Eq)
-- >>> data Employee = Employee { name :: String, department :: Department, age :: Int }
--
-- We define some employees that we will display in a table:
--
-- >>> :{
-- let employees =
-- [ Employee "Thaddeus" Sales 34
-- , Employee "Lucia" Engineering 33
-- , Employee "Pranav" Management 57
-- ]
-- :}
--
-- Let's build a table that displays the name and the age
-- of an employee. Additionally, we will emphasize the names of
-- engineers using a @\<strong\>@ tag.
--
-- >>> :{
-- let tableEmpA :: Colonnade Headed Employee Html
-- tableEmpA = mconcat
-- [ headed "Name" $ \emp -> case department emp of
-- Engineering -> H.strong (toHtml (name emp))
-- _ -> toHtml (name emp)
-- , headed "Age" (toHtml . show . age)
-- ]
-- :}
--
-- The type signature of @tableEmpA@ is inferrable but is written
-- out for clarity in this example. Additionally, note that the first
-- argument to 'headed' is of type 'Html', so @OverloadedStrings@ is
-- necessary for the above example to compile. To avoid using this extension,
-- it is possible to instead use 'toHtml' to convert a 'String' to 'Html'.
-- Let\'s continue:
--
-- >>> let customAttrs = HA.class_ "stylish-table" <> HA.id "main-table"
-- >>> printCompactHtml (encodeHtmlTable customAttrs tableEmpA employees)
-- <table class="stylish-table" id="main-table">
-- <thead>
-- <tr>
-- <th>Name</th>
-- <th>Age</th>
-- </tr>
-- </thead>
-- <tbody>
-- <tr>
-- <td>Thaddeus</td>
-- <td>34</td>
-- </tr>
-- <tr>
-- <td><strong>Lucia</strong></td>
-- <td>33</td>
-- </tr>
-- <tr>
-- <td>Pranav</td>
-- <td>57</td>
-- </tr>
-- </tbody>
-- </table>
--
-- Excellent. As expected, Lucia\'s name is wrapped in a @\<strong\>@ tag
-- since she is an engineer.
--
-- One limitation of using 'Html' as the content
-- type of a 'Colonnade' is that we are unable to add attributes to
-- the @\<td\>@ and @\<th\>@ elements. This library provides the 'Cell' type
-- to work around this problem. A 'Cell' is just 'Html' content and a set
-- of attributes to be applied to its parent @<th>@ or @<td>@. To illustrate
-- how its use, another employee table will be built. This table will
-- contain a single column indicating the department of each employ. Each
-- cell will be assigned a class name based on the department. To start off,
-- let\'s build a table that encodes departments:
--
-- >>> :{
-- let tableDept :: Colonnade Headed Department Cell
-- tableDept = mconcat
-- [ headed "Dept." $ \d -> Cell
-- (HA.class_ (toValue (map toLower (show d))))
-- (toHtml (show d))
-- ]
-- :}
--
-- Again, @OverloadedStrings@ plays a role, this time allowing the
-- 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
-- 'encodeCellTable' instead of 'encodeHtmlTable':
--
-- >>> let twoDepts = [Sales,Management]
-- >>> printVeryCompactHtml (encodeCellTable customAttrs tableDept twoDepts)
-- <table class="stylish-table" id="main-table">
-- <thead>
-- <tr><th>Dept.</th></tr>
-- </thead>
-- <tbody>
-- <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 @Profunctor@ instance of 'Colonnade' to allow
-- this to work on @Employee@\'s instead:
--
-- >>> :t lmap
-- lmap :: Profunctor p => (a -> b) -> p b c -> p a c
-- >>> let tableEmpB = lmap department tableDept
-- >>> :t tableEmpB
-- tableEmpB :: Colonnade Headed Employee Cell
-- >>> printVeryCompactHtml (encodeCellTable customAttrs tableEmpB employees)
-- <table class="stylish-table" id="main-table">
-- <thead>
-- <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>
-- </tbody>
-- </table>
--
-- This table shows the department of each of our three employees, additionally
-- making a lowercased version of the department into a class name for the @\<td\>@.
-- This table is nice for illustrative purposes, but it does not provide all the
-- information that we have about the employees. If we combine it with the
-- earlier table we wrote, we can present everything in the table. One small
-- roadblock is that the types of @tableEmpA@ and @tableEmpB@ do not match, which
-- prevents a straightforward monoidal append:
--
-- >>> :t tableEmpA
-- tableEmpA :: Colonnade Headed Employee Html
-- >>> :t tableEmpB
-- tableEmpB :: Colonnade Headed Employee Cell
--
-- 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 = fmap htmlCell tableEmpA <> tableEmpB
-- >>> :t tableEmpC
-- tableEmpC :: Colonnade Headed Employee Cell
-- >>> printCompactHtml (encodeCellTable customAttrs tableEmpC employees)
-- <table class="stylish-table" id="main-table">
-- <thead>
-- <tr>
-- <th>Name</th>
-- <th>Age</th>
-- <th>Dept.</th>
-- </tr>
-- </thead>
-- <tbody>
-- <tr>
-- <td>Thaddeus</td>
-- <td>34</td>
-- <td class="sales">Sales</td>
-- </tr>
-- <tr>
-- <td><strong>Lucia</strong></td>
-- <td>33</td>
-- <td class="engineering">Engineering</td>
-- </tr>
-- <tr>
-- <td>Pranav</td>
-- <td>57</td>
-- <td class="management">Management</td>
-- </tr>
-- </tbody>
-- </table>
-- $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 = Cell
{ cellAttribute :: !Attribute
, cellHtml :: !Html
}
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 = (<>)
-- | Create a 'Cell' from a 'Widget'
htmlCell :: Html -> Cell
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. 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 :: 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'
-> Attribute -- ^ Attributes of @\<table\>@ element
-> Colonnade h a c -- ^ How to encode data as a row
-> f a -- ^ Collection of data
-> Html
encodeTable mtheadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
H.table ! tableAttrs $ do
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
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 Headed p a c
-> f a -- ^ Collection of data
-> Html
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 $ 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 f
=> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element
-> ((Html -> Html) -> c -> Html) -- ^ 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
encodeBody trAttrs wrapContent tbodyAttrs colonnade xs = do
H.tbody ! tbodyAttrs $ do
forM_ xs $ \x -> do
H.tr ! trAttrs x $ E.rowMonoidal colonnade (wrapContent H.td) x
-- | Encode a table. Table cells may have attributes
-- applied to them.
encodeCellTable ::
Foldable f
=> Attribute -- ^ Attributes of @\<table\>@ element
-> Colonnade Headed a Cell -- ^ How to encode data as columns
-> f a -- ^ Collection of data
-> Html
encodeCellTable = encodeTable
(E.headednessPure (mempty,mempty)) mempty (const mempty) htmlFromCell
-- | 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 h a Html -- ^ How to encode data as columns
-> f a -- ^ Collection of data
-> Html
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
data St = St
{ stContext :: [String]
, stTagStatus :: TagStatus
, stResult :: String -> String -- ^ difference list
}
data TagStatus
= TagStatusSomeTag
| TagStatusOpening (String -> String)
| TagStatusOpeningAttrs
| TagStatusNormal
| TagStatusClosing (String -> String)
| TagStatusAfterTag
removeWhitespaceAfterTag :: String -> String -> String
removeWhitespaceAfterTag chosenTag =
either id (\st -> stResult st "") . foldlM (flip f) (St [] TagStatusNormal id)
where
f :: Char -> St -> Either String St
f c (St ctx status res) = case status of
TagStatusNormal
| c == '<' -> Right (St ctx TagStatusSomeTag likelyRes)
| isSpace c -> if Just chosenTag == listToMaybe ctx
then Right (St ctx TagStatusNormal res) -- drops the whitespace
else Right (St ctx TagStatusNormal likelyRes)
| otherwise -> Right (St ctx TagStatusNormal likelyRes)
TagStatusSomeTag
| c == '/' -> Right (St ctx (TagStatusClosing id) likelyRes)
| c == '>' -> Left "unexpected >"
| c == '<' -> Left "unexpected <"
| otherwise -> Right (St ctx (TagStatusOpening (c:)) likelyRes)
TagStatusOpening tag
| c == '>' -> Right (St (tag "" : ctx) TagStatusAfterTag likelyRes)
| isSpace c -> Right (St (tag "" : ctx) TagStatusOpeningAttrs likelyRes)
| otherwise -> Right (St ctx (TagStatusOpening (tag . (c:))) likelyRes)
TagStatusOpeningAttrs
| c == '>' -> Right (St ctx TagStatusAfterTag likelyRes)
| otherwise -> Right (St ctx TagStatusOpeningAttrs likelyRes)
TagStatusClosing tag
| c == '>' -> do
otherTags <- case ctx of
[] -> Left "closing tag without any opening tag"
closestTag : otherTags -> if closestTag == tag ""
then Right otherTags
else Left $ "closing tag <" ++ tag "" ++ "> did not match opening tag <" ++ closestTag ++ ">"
Right (St otherTags TagStatusAfterTag likelyRes)
| otherwise -> Right (St ctx (TagStatusClosing (tag . (c:))) likelyRes)
TagStatusAfterTag
| c == '<' -> Right (St ctx TagStatusSomeTag likelyRes)
| isSpace c -> if Just chosenTag == listToMaybe ctx
then Right (St ctx TagStatusAfterTag res) -- drops the whitespace
else Right (St ctx TagStatusNormal likelyRes)
| otherwise -> Right (St ctx TagStatusNormal likelyRes)
where
likelyRes :: String -> String
likelyRes = res . (c:)
-- | Pretty print an HTML table, stripping whitespace from inside @\<td\>@,
-- @\<th\>@, and common inline tags. The implementation is inefficient and is
-- incorrect in many corner cases. It is only provided to reduce the line
-- count of the HTML printed by GHCi examples in this module\'s documentation.
-- Use of this function is discouraged.
printCompactHtml :: Html -> IO ()
printCompactHtml = putStrLn
. List.dropWhileEnd (== '\n')
. removeWhitespaceAfterTag "td"
. removeWhitespaceAfterTag "th"
. removeWhitespaceAfterTag "strong"
. removeWhitespaceAfterTag "span"
. removeWhitespaceAfterTag "em"
. Pretty.renderHtml
-- | Similar to 'printCompactHtml'. Additionally strips all whitespace inside
-- @\<tr\>@ elements and @\<thead\>@ elements.
printVeryCompactHtml :: Html -> IO ()
printVeryCompactHtml = putStrLn
. List.dropWhileEnd (== '\n')
. removeWhitespaceAfterTag "td"
. removeWhitespaceAfterTag "th"
. removeWhitespaceAfterTag "strong"
. removeWhitespaceAfterTag "span"
. removeWhitespaceAfterTag "em"
. removeWhitespaceAfterTag "tr"
. Pretty.renderHtml
-- $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 Cell a -> f a -> Html
--
-- The 'Colonnade' content type is 'Cell', but the content
-- type of the result is 'Html'. It may not be immidiately clear why
-- this is useful 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 Html a -> f a -> Html
--
-- 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.

16
build
View File

@ -1,16 +0,0 @@
#!/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

View File

@ -1,5 +0,0 @@
packages: ./colonnade
./blaze-colonnade
./lucid-colonnade
./siphon
./yesod-colonnade

View File

@ -1,6 +1,9 @@
name: colonnade
version: 1.2.0.2
synopsis: Generic types and functions for columnar encoding and decoding
cabal-version: 3.0
name: colonnade
version: 1.2.0.3
synopsis:
Generic types and functions for columnar encoding and decoding
description:
The `colonnade` package provides a way to talk about
columnar encodings and decodings of data. This package provides
@ -19,32 +22,39 @@ description:
* <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/byteverse/colonnade
bug-reports: https://github.com/byteverse/colonnade/issues
license: BSD-3-Clause
license-file: LICENSE
author: Andrew Martin
maintainer: amartin@layer3com.com
copyright: 2016 Andrew Martin
category: web
build-type: Simple
extra-doc-files:
CHANGELOG.md
README.md
tested-with: GHC ==9.4.8 || ==9.6.3 || ==9.8.1
common build-settings
default-language: Haskell2010
ghc-options: -Wall -Wunused-packages
build-depends: base >=4.12 && <5
library
hs-source-dirs: src
import: build-settings
ghc-options: -O2
hs-source-dirs: src
exposed-modules:
Colonnade
Colonnade.Encode
build-depends:
base >= 4.12 && < 5
, contravariant >= 1.2 && < 1.6
, vector >= 0.10 && < 0.14
, text >= 1.0 && < 2.1
, bytestring >= 0.10 && < 0.12
, profunctors >= 5.0 && < 5.7
, semigroups >= 0.18.2 && < 0.21
default-language: Haskell2010
ghc-options: -Wall
, profunctors >=5.0
, vector >=0.10
source-repository head
type: git
location: https://github.com/andrewthad/colonnade
location: git://github.com/byteverse/colonnade.git

View File

@ -1,30 +0,0 @@
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.

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,48 +0,0 @@
#!/bin/bash
set -e
if [ "$#" -ne 1 ]; then
echo "Usage: scripts/hackage-docs.sh HACKAGE_USER"
exit 1
fi
user=$1
cabal_file=$(find . -maxdepth 1 -name "*.cabal" -print -quit)
if [ ! -f "$cabal_file" ]; then
echo "Run this script in the top-level package directory"
exit 1
fi
pkg=$(awk -F ":[[:space:]]*" 'tolower($1)=="name" { print $2 }' < "$cabal_file")
ver=$(awk -F ":[[:space:]]*" 'tolower($1)=="version" { print $2 }' < "$cabal_file")
if [ -z "$pkg" ]; then
echo "Unable to determine package name"
exit 1
fi
if [ -z "$ver" ]; then
echo "Unable to determine package version"
exit 1
fi
echo "Detected package: $pkg-$ver"
dir=$(mktemp -d build-docs.XXXXXX)
trap 'rm -r "$dir"' EXIT
# cabal haddock --hoogle --hyperlink-source --html-location='/package/$pkg-$version/docs' --contents-location='/package/$pkg-$version'
stack haddock
cp -R .stack-work/dist/x86_64-linux/Cabal-1.22.5.0/doc/html/$pkg/ $dir/$pkg-$ver-docs
# /home/andrew/.stack/snapshots/x86_64-linux/lts-5.17/7.10.3/doc/index.html
tar cvz -C $dir --format=ustar -f $dir/$pkg-$ver-docs.tar.gz $pkg-$ver-docs
curl -X PUT \
-H 'Content-Type: application/x-tar' \
-H 'Content-Encoding: gzip' \
-u "$user" \
--data-binary "@$dir/$pkg-$ver-docs.tar.gz" \
"https://hackage.haskell.org/package/$pkg-$ver/docs"

View File

@ -1,438 +0,0 @@
{-# 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.
module Colonnade
( -- * Example
-- $setup
-- * Types
Colonnade
, Headed(..)
, Headless(..)
-- * Typeclasses
, E.Headedness(..)
-- * Create
, headed
, headless
, singleton
-- * Transform
-- ** Body
, fromMaybe
, columns
, bool
, replaceWhen
, modifyWhen
-- ** Header
, mapHeaderContent
, mapHeadedness
, toHeadless
-- * Cornice
-- ** Types
, Cornice
, Pillar(..)
, Fascia(..)
-- ** Create
, cap
, recap
-- * Ascii Table
, ascii
, asciiCapped
) where
import Colonnade.Encode (Colonnade,Cornice,
Pillar(..),Fascia(..),Headed(..),Headless(..))
import Data.Foldable
import Control.Monad
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
-- $setup
--
-- First, let\'s bring in some neccessary imports that will be
-- used for the remainder of the examples in the docs:
--
-- >>> import Data.Monoid (mconcat,(<>))
-- >>> import Data.Profunctor (lmap)
--
-- The data types we wish to encode are:
--
-- >>> data Color = Red | Green | Blue deriving (Show,Eq)
-- >>> data Person = Person { name :: String, age :: Int }
-- >>> data House = House { color :: Color, price :: Int }
--
-- One potential columnar encoding of a @Person@ would be:
--
-- >>> :{
-- let colPerson :: Colonnade Headed Person String
-- colPerson = mconcat
-- [ headed "Name" name
-- , headed "Age" (show . age)
-- ]
-- :}
--
-- The type signature on @colPerson@ is not neccessary
-- but is included for clarity. We can feed data into this encoding
-- to build a table:
--
-- >>> let people = [Person "David" 63, Person "Ava" 34, Person "Sonia" 12]
-- >>> putStr (ascii colPerson people)
-- +-------+-----+
-- | Name | Age |
-- +-------+-----+
-- | David | 63 |
-- | Ava | 34 |
-- | Sonia | 12 |
-- +-------+-----+
--
-- Similarly, we can build a table of houses with:
--
-- >>> let showDollar = (('$':) . show) :: Int -> String
-- >>> colHouse = mconcat [headed "Color" (show . color), headed "Price" (showDollar . price)]
-- >>> :t colHouse
-- colHouse :: Colonnade Headed House String
-- >>> let houses = [House Green 170000, House Blue 115000, House Green 150000]
-- >>> putStr (ascii colHouse houses)
-- +-------+---------+
-- | Color | Price |
-- +-------+---------+
-- | Green | $170000 |
-- | Blue | $115000 |
-- | Green | $150000 |
-- +-------+---------+
-- | A single column with a header.
headed :: c -> (a -> c) -> Colonnade Headed a c
headed h = singleton (Headed h)
-- | A single column without a header.
headless :: (a -> c) -> Colonnade Headless a c
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 = 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 (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
-- could be represented as:
--
-- >>> :{
-- let owners :: [(Person,Maybe House)]
-- owners =
-- [ (Person "Jordan" 18, Nothing)
-- , (Person "Ruth" 25, Just (House Red 125000))
-- , (Person "Sonia" 12, Just (House Green 145000))
-- ]
-- :}
--
-- The column encodings defined earlier can be reused with
-- the help of 'fromMaybe':
--
-- >>> :{
-- let colOwners :: Colonnade Headed (Person,Maybe House) String
-- colOwners = mconcat
-- [ lmap fst colPerson
-- , lmap snd (fromMaybe "" colHouse)
-- ]
-- :}
--
-- >>> putStr (ascii colOwners owners)
-- +--------+-----+-------+---------+
-- | Name | Age | Color | Price |
-- +--------+-----+-------+---------+
-- | Jordan | 18 | | |
-- | Ruth | 25 | Red | $125000 |
-- | Sonia | 12 | Green | $145000 |
-- +--------+-----+-------+---------+
fromMaybe :: c -> Colonnade f a c -> Colonnade f (Maybe a) c
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
-- by putting a check mark in the column corresponding to
-- the color instead of by writing out the name of the color:
--
-- >>> let allColors = [Red,Green,Blue]
-- >>> let encColor = columns (\c1 c2 -> if c1 == c2 then "✓" else "") (Headed . show) allColors
-- >>> :t encColor
-- encColor :: Colonnade Headed Color String
-- >>> let encHouse = headed "Price" (showDollar . price) <> lmap color encColor
-- >>> :t encHouse
-- encHouse :: Colonnade Headed House String
-- >>> putStr (ascii encHouse houses)
-- +---------+-----+-------+------+
-- | Price | Red | Green | Blue |
-- +---------+-----+-------+------+
-- | $170000 | | ✓ | |
-- | $115000 | | | ✓ |
-- | $150000 | | ✓ | |
-- +---------+-----+-------+------+
columns :: Foldable g
=> (b -> a -> c) -- ^ Cell content function
-> (b -> f c) -- ^ Header content function
-> g b -- ^ Basis for column encodings
-> Colonnade f a c
columns getCell getHeader = id
. E.Colonnade
. Vector.map (\b -> E.OneColonnade (getHeader b) (getCell b))
. Vector.fromList
. toList
bool ::
f c -- ^ Heading
-> (a -> Bool) -- ^ Predicate
-> (a -> c) -- ^ Contents when predicate is false
-> (a -> c) -- ^ Contents when predicate is true
-> Colonnade f a c
bool h p onTrue onFalse = singleton h (Data.Bool.bool <$> onFalse <*> onTrue <*> p)
-- | Modify the contents of cells in rows whose values satisfy the
-- given predicate. Header content is unaffected. With an HTML backend,
-- this can be used to strikethrough the contents of cells with data that is
-- considered invalid.
modifyWhen ::
(c -> c) -- ^ Content change
-> (a -> Bool) -- ^ Row predicate
-> Colonnade f a c -- ^ Original 'Colonnade'
-> Colonnade f a c
modifyWhen changeContent p (E.Colonnade v) = E.Colonnade
( Vector.map
(\(E.OneColonnade h encode) -> E.OneColonnade h $ \a ->
if p a then changeContent (encode a) else encode a
) v
)
-- | Replace the contents of cells in rows whose values satisfy the
-- given predicate. Header content is unaffected.
replaceWhen ::
c -- ^ New content
-> (a -> Bool) -- ^ Row predicate
-> Colonnade f a c -- ^ Original 'Colonnade'
-> Colonnade f a c
replaceWhen = modifyWhen . const
-- | Augment a 'Colonnade' with a header spans over all of the
-- existing headers. This is best demonstrated by example.
-- Let\'s consider how we might encode a pairing of the people
-- and houses from the initial example:
--
-- >>> let personHomePairs = zip people houses
-- >>> let colPersonFst = lmap fst colPerson
-- >>> let colHouseSnd = lmap snd colHouse
-- >>> putStr (ascii (colPersonFst <> colHouseSnd) personHomePairs)
-- +-------+-----+-------+---------+
-- | Name | Age | Color | Price |
-- +-------+-----+-------+---------+
-- | David | 63 | Green | $170000 |
-- | Ava | 34 | Blue | $115000 |
-- | Sonia | 12 | Green | $150000 |
-- +-------+-----+-------+---------+
--
-- This tabular encoding leaves something to be desired. The heading
-- not indicate that the name and age refer to a person and that
-- the color and price refer to a house. Without reaching for 'Cornice',
-- we can still improve this situation with 'mapHeaderContent':
--
-- >>> let colPersonFst' = mapHeaderContent ("Person " ++) colPersonFst
-- >>> let colHouseSnd' = mapHeaderContent ("House " ++) colHouseSnd
-- >>> putStr (ascii (colPersonFst' <> colHouseSnd') personHomePairs)
-- +-------------+------------+-------------+-------------+
-- | Person Name | Person Age | House Color | House Price |
-- +-------------+------------+-------------+-------------+
-- | David | 63 | Green | $170000 |
-- | Ava | 34 | Blue | $115000 |
-- | Sonia | 12 | Green | $150000 |
-- +-------------+------------+-------------+-------------+
--
-- This is much better, but for longer tables, the redundancy
-- of prefixing many column headers can become annoying. The solution
-- that a 'Cornice' offers is to nest headers:
--
-- >>> let cor = mconcat [cap "Person" colPersonFst, cap "House" colHouseSnd]
-- >>> :t cor
-- cor :: Cornice Headed ('Cap 'Base) (Person, House) String
-- >>> putStr (asciiCapped cor personHomePairs)
-- +-------------+-----------------+
-- | Person | House |
-- +-------+-----+-------+---------+
-- | Name | Age | Color | Price |
-- +-------+-----+-------+---------+
-- | David | 63 | Green | $170000 |
-- | Ava | 34 | Blue | $115000 |
-- | Sonia | 12 | Green | $150000 |
-- +-------+-----+-------+---------+
--
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:
--
-- >>> data Day = Weekday | Weekend deriving (Show)
-- >>> :{
-- let cost :: Int -> Day -> String
-- cost base w = case w of
-- Weekday -> showDollar base
-- Weekend -> showDollar (base + 1)
-- colStandard = foldMap (\c -> headed c (cost 8)) ["Yt","Ad","Sr"]
-- colSpecial = mconcat [headed "Stud" (cost 6), headed "Mltry" (cost 7)]
-- corStatus = mconcat
-- [ cap "Standard" colStandard
-- , cap "Special" colSpecial
-- ]
-- corShowtime = mconcat
-- [ recap "" (cap "" (headed "Day" show))
-- , foldMap (\c -> recap c corStatus) ["Matinee","Evening"]
-- ]
-- :}
--
-- >>> putStr (asciiCapped corShowtime [Weekday,Weekend])
-- +---------+-----------------------------+-----------------------------+
-- | | Matinee | Evening |
-- +---------+--------------+--------------+--------------+--------------+
-- | | Standard | Special | Standard | Special |
-- +---------+----+----+----+------+-------+----+----+----+------+-------+
-- | Day | Yt | Ad | Sr | Stud | Mltry | Yt | Ad | Sr | Stud | Mltry |
-- +---------+----+----+----+------+-------+----+----+----+------+-------+
-- | 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 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 Headed p a String -- ^ columnar encoding
-> f a -- ^ rows
-> String
asciiCapped cor xs =
let annCor = E.annotateFinely (\x y -> x + y + 3) id
List.length xs cor
sizedCol = E.uncapAnnotated annCor
in E.headersMonoidal
Nothing
[ ( \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
-- | Render a collection of rows as an ascii table. The table\'s columns are
-- specified by the given 'Colonnade'. This implementation is inefficient and
-- does not provide any wrapping behavior. It is provided so that users can
-- try out @colonnade@ in ghci and so that @doctest@ can verify example
-- code in the haddocks.
ascii :: Foldable f
=> Colonnade Headed a String -- ^ columnar encoding
-> f a -- ^ rows
-> String
ascii col xs =
let sizedCol = E.sizeColumns List.length xs col
divider = concat
[ E.headerMonoidalFull sizedCol
(\(E.Sized msz _) -> case msz of
Just sz -> "+" ++ hyphens (sz + 2)
Nothing -> ""
)
, "+\n"
]
in List.concat
[ divider
, concat
[ 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 (E.Sized (Maybe Int) Headed) a String
-> f a
-> String
asciiBody sizedCol xs =
let divider = concat
[ E.headerMonoidalFull sizedCol
(\(E.Sized msz _) -> case msz of
Just sz -> "+" ++ hyphens (sz + 2)
Nothing -> ""
)
, "+\n"
]
rowContents = foldMap
(\x -> concat
[ E.rowMonoidalHeader
sizedCol
(\(E.Sized msz _) c -> case msz of
Nothing -> ""
Just sz -> "| " ++ rightPad sz ' ' c ++ " "
)
x
, "|\n"
]
) xs
in List.concat
[ divider
, rowContents
, divider
]
hyphens :: Int -> String
hyphens n = List.replicate n '-'
rightPad :: Int -> a -> [a] -> [a]
rightPad m a xs = take m $ xs ++ repeat a
-- data Company = Company String String Int
--
-- data Company = Company
-- { companyName :: String
-- , companyCountry :: String
-- , companyValue :: Int
-- } deriving (Show)
--
-- myCompanies :: [Company]
-- myCompanies =
-- [ Company "eCommHub" "United States" 50
-- , Company "Layer 3 Communications" "United States" 10000000
-- , Company "Microsoft" "England" 500000000
-- ]

View File

@ -1,691 +0,0 @@
{-# 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,
-- a function that applies a @Colonnade Headed MyCell a@
-- to data will have roughly the following type:
--
-- > myTableRenderer :: Foldable g => Colonnade Headed MyCell a -> g a -> MyContent
--
-- In the companion packages @yesod-colonnade@ and
-- @reflex-dom-colonnade@, functions with
-- similar type signatures are readily available.
-- These packages use the functions provided here
-- in the implementations of their rendering functions.
-- It is recommended that users who believe they may need
-- this module look at the source of the companion packages
-- to see an example of how this module\'s functions are used.
-- Other backends are encouraged to use these functions
-- to build monadic or monoidal content from a 'Colonnade'.
--
-- The functions exported here take a 'Colonnade' and
-- convert it to a fragment of content. The functions whose
-- names start with @row@ take at least a @Colonnade f c a@ and an @a@
-- value to generate a row of content. The functions whose names
-- start with @header@ need the @Colonnade f c a@ but not
-- an @a@ value since a value is not needed to build a header.
--
module Colonnade.Encode
( -- * Colonnade
-- ** Types
Colonnade(..)
, OneColonnade(..)
, Headed(..)
, Headless(..)
, Sized(..)
, ExtractForall(..)
-- ** Typeclasses
, Headedness(..)
-- ** Row
, row
, rowMonadic
, rowMonadic_
, rowMonadicWith
, rowMonoidal
, rowMonoidalHeader
-- ** Header
, header
, headerMonadic
, headerMonadic_
, headerMonadicGeneral
, headerMonadicGeneral_
, headerMonoidalGeneral
, headerMonoidalFull
-- ** Other
, bothMonadic_
, sizeColumns
-- * Cornice
-- ** Types
, Cornice(..)
, AnnotatedCornice(..)
, OneCornice(..)
, Pillar(..)
, ToEmptyCornice(..)
, Fascia(..)
-- ** Encoding
, annotate
, annotateFinely
, size
, endow
, discard
, headersMonoidal
, uncapAnnotated
) where
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
-- instead. It may allow more things to get inlined
-- in to a loop.
row :: (c1 -> c2) -> Colonnade f a c1 -> a -> Vector c2
row g (Colonnade v) a = flip Vector.map v $
\(OneColonnade _ encode) -> g (encode a)
bothMonadic_ :: Monad m
=> Colonnade Headed a c
-> (c -> c -> m b)
-> a
-> m ()
bothMonadic_ (Colonnade v) g a =
forM_ v $ \(OneColonnade (Headed h) encode) -> g h (encode a)
rowMonadic ::
(Monad m, Monoid b)
=> Colonnade f a c
-> (c -> m b)
-> a
-> m b
rowMonadic (Colonnade v) g a =
flip foldlMapM v
$ \e -> g (oneColonnadeEncode e a)
rowMonadic_ ::
Monad m
=> Colonnade f a c
-> (c -> m b)
-> a
-> m ()
rowMonadic_ (Colonnade v) g a =
forM_ v $ \e -> g (oneColonnadeEncode e a)
rowMonoidal ::
Monoid m
=> Colonnade h a c
-> (c -> m)
-> a
-> m
rowMonoidal (Colonnade v) g a =
foldMap (\(OneColonnade _ encode) -> g (encode a)) v
rowMonoidalHeader ::
Monoid m
=> Colonnade h a c
-> (h c -> c -> m)
-> a
-> m
rowMonoidalHeader (Colonnade v) g a =
foldMap (\(OneColonnade h encode) -> g h (encode a)) v
rowUpdateSize ::
(c -> Int) -- ^ Get size from content
-> MutableSizedColonnade s h a c
-> a
-> ST s ()
rowUpdateSize toSize (MutableSizedColonnade v mv) a = if MVU.length mv /= V.length v
then error "rowMonoidalSize: vector sizes mismatched"
else V.imapM_ (\ix (OneColonnade _ encode) ->
MVU.modify mv (\oldSize -> max oldSize (toSize (encode a))) ix
) v
headerUpdateSize :: Foldable h
=> (c -> Int) -- ^ Get size from content
-> MutableSizedColonnade s h a c
-> ST s ()
headerUpdateSize toSize (MutableSizedColonnade v mv) = if MVU.length mv /= V.length v
then error "rowMonoidalSize: vector sizes mismatched"
else V.imapM_ (\ix (OneColonnade h _) ->
MVU.modify mv (\oldSize -> max oldSize (foldl' (\sz c -> max sz (toSize c)) 0 h)) ix
) v
sizeColumns :: (Foldable f, Foldable h)
=> (c -> Int) -- ^ Get size from content
-> f a
-> Colonnade h a c
-> Colonnade (Sized (Maybe Int) h) a c
sizeColumns toSize rows colonnade = runST $ do
mcol <- newMutableSizedColonnade colonnade
headerUpdateSize toSize mcol
mapM_ (rowUpdateSize toSize mcol) rows
freezeMutableSizedColonnade mcol
newMutableSizedColonnade :: Colonnade h a c -> ST s (MutableSizedColonnade s h a c)
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 (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 (Just sz) h) enc)
$ V.zip v (GV.convert sizeVec)
rowMonadicWith ::
(Monad m)
=> b
-> (b -> b -> b)
-> Colonnade f a c
-> (c -> m b)
-> a
-> m b
rowMonadicWith bempty bappend (Colonnade v) g a =
foldlM (\bl e -> do
br <- g (oneColonnadeEncode e a)
return (bappend bl br)
) bempty v
header :: (c1 -> c2) -> Colonnade Headed a c1 -> Vector c2
header g (Colonnade v) =
Vector.map (g . getHeaded . oneColonnadeHead) v
-- | This function is a helper for abusing 'Foldable' to optionally
-- render a header. Its future is uncertain.
headerMonadicGeneral :: (Monad m, Monoid b, Foldable h)
=> Colonnade h a c
-> (c -> m b)
-> m b
headerMonadicGeneral (Colonnade v) g = id
$ fmap (mconcat . Vector.toList)
$ Vector.mapM (foldlMapM g . oneColonnadeHead) v
headerMonadic ::
(Monad m, Monoid b)
=> Colonnade Headed a c
-> (c -> m b)
-> m b
headerMonadic (Colonnade v) g =
fmap (mconcat . Vector.toList) $ Vector.mapM (g . getHeaded . oneColonnadeHead) v
headerMonadicGeneral_ ::
(Monad m, Headedness h)
=> Colonnade h a c
-> (c -> m b)
-> m ()
headerMonadicGeneral_ (Colonnade v) g = case headednessExtract of
Nothing -> return ()
Just f -> Vector.mapM_ (g . f . oneColonnadeHead) v
headerMonoidalGeneral ::
(Monoid m, Foldable h)
=> Colonnade h a c
-> (c -> m)
-> m
headerMonoidalGeneral (Colonnade v) g =
foldMap (foldMap g . oneColonnadeHead) v
headerMonoidalFull ::
Monoid m
=> Colonnade h a c
-> (h c -> m)
-> m
headerMonoidalFull (Colonnade v) g = foldMap (g . oneColonnadeHead) v
headerMonadic_ ::
(Monad m)
=> Colonnade Headed a c
-> (c -> m b)
-> m ()
headerMonadic_ (Colonnade v) g = Vector.mapM_ (g . getHeaded . oneColonnadeHead) v
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 }

51
fourmolu.yaml Normal file
View File

@ -0,0 +1,51 @@
# Number of spaces per indentation step
indentation: 2
# Max line length for automatic line breaking
column-limit: 200
# Styling of arrows in type signatures (choices: trailing, leading, or leading-args)
function-arrows: trailing
# How to place commas in multi-line lists, records, etc. (choices: leading or trailing)
comma-style: leading
# Styling of import/export lists (choices: leading, trailing, or diff-friendly)
import-export-style: leading
# Whether to full-indent or half-indent 'where' bindings past the preceding body
indent-wheres: false
# Whether to leave a space before an opening record brace
record-brace-space: true
# Number of spaces between top-level declarations
newlines-between-decls: 1
# How to print Haddock comments (choices: single-line, multi-line, or multi-line-compact)
haddock-style: multi-line
# How to print module docstring
haddock-style-module: null
# Styling of let blocks (choices: auto, inline, newline, or mixed)
let-style: auto
# How to align the 'in' keyword with respect to the 'let' keyword (choices: left-align, right-align, or no-space)
in-style: right-align
# Whether to put parentheses around a single constraint (choices: auto, always, or never)
single-constraint-parens: always
# Output Unicode syntax (choices: detect, always, or never)
unicode: never
# Give the programmer more choice on where to insert blank lines
respectful: true
# Fixity information for operators
fixities: []
# Module reexports Fourmolu should know about
reexports: []

View File

@ -1,30 +0,0 @@
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.

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,11 +0,0 @@
network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
24.165.56.0/22,5848280,6252001,,0,0,96746,22.0837,-159.3553,10
78.146.173.128/25,2655583,2635167,,0,0,DL14,54.6500,-1.6667,20
121.211.108.0/23,2160386,2077456,,0,0,2040,-33.8833,151.1500,5
69.74.43.16/30,6252001,6252001,,0,0,,37.7510,-97.8220,1000
77.128.35.136/30,3034803,3017382,,0,0,57450,49.0667,6.8333,20
90.54.234.0/24,2977062,3017382,,0,0,49320,47.3944,-0.4357,50
77.193.41.175/32,3018587,3017382,,0,0,78810,48.8700,1.9740,1
58.188.32.0/24,1861060,1861060,,0,0,,35.6900,139.6900,500
87.81.232.0/24,2635167,2635167,,0,0,,51.4964,-0.1224,200
88.191.56.0/22,2988507,3017382,,0,0,75001,48.8667,2.3333,500
1 network geoname_id registered_country_geoname_id represented_country_geoname_id is_anonymous_proxy is_satellite_provider postal_code latitude longitude accuracy_radius
2 24.165.56.0/22 5848280 6252001 0 0 96746 22.0837 -159.3553 10
3 78.146.173.128/25 2655583 2635167 0 0 DL14 54.6500 -1.6667 20
4 121.211.108.0/23 2160386 2077456 0 0 2040 -33.8833 151.1500 5
5 69.74.43.16/30 6252001 6252001 0 0 37.7510 -97.8220 1000
6 77.128.35.136/30 3034803 3017382 0 0 57450 49.0667 6.8333 20
7 90.54.234.0/24 2977062 3017382 0 0 49320 47.3944 -0.4357 50
8 77.193.41.175/32 3018587 3017382 0 0 78810 48.8700 1.9740 1
9 58.188.32.0/24 1861060 1861060 0 0 35.6900 139.6900 500
10 87.81.232.0/24 2635167 2635167 0 0 51.4964 -0.1224 200
11 88.191.56.0/22 2988507 3017382 0 0 75001 48.8667 2.3333 500

View File

@ -1,21 +0,0 @@
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone
2653810,en,EU,Europe,GB,"United Kingdom",SCT,Scotland,GLG,"Glasgow City",Cardonald,,Europe/London
2832529,en,EU,Europe,DE,Germany,RP,Rheinland-Pfalz,,,Siefersheim,,Europe/Berlin
2885499,en,EU,Europe,DE,Germany,MV,Mecklenburg-Vorpommern,,,Koerchow,,Europe/Berlin
550870,en,EU,Europe,RU,Russia,NIZ,"Nizhegorodskaya Oblast'",,,Khabarskoye,,Europe/Moscow
766583,en,EU,Europe,PL,Poland,LU,"Lublin Voivodeship",,,Leczna,,Europe/Warsaw
2608246,en,EU,Europe,AT,Austria,1,Burgenland,,,"Neuhaus am Klausenbach",,Europe/Vienna
5121765,en,NA,"North America",US,"United States",NY,"New York",,,Ilion,526,America/New_York
2935825,en,EU,Europe,DE,Germany,NW,"North Rhine-Westphalia",,,Dormagen,,Europe/Berlin
3165189,en,EU,Europe,IT,Italy,36,"Friuli Venezia Giulia",UD,"Provincia di Udine",Tricesimo,,Europe/Rome
4564070,en,NA,"North America",PR,"Puerto Rico",,,,,Culebra,,America/Puerto_Rico
2993759,en,EU,Europe,FR,France,U,"Provence-Alpes-Côte d'Azur",13,Bouches-du-Rhône,Miramas-le-Vieux,,Europe/Paris
5861117,en,NA,"North America",US,"United States",AK,Alaska,,,"Dutch Harbor",743,America/Adak
4375229,en,NA,"North America",US,"United States",MO,Missouri,,,Ashland,604,America/Chicago
2946980,en,EU,Europe,DE,Germany,SN,Saxony,,,Boehlen,,Europe/Berlin
3156470,en,EU,Europe,NO,Norway,02,Akershus,,,Frogner,,Europe/Oslo
3166193,en,EU,Europe,IT,Italy,36,"Friuli Venezia Giulia",GO,"Provincia di Gorizia",Staranzano,,Europe/Rome
4913742,en,NA,"North America",US,"United States",IL,Illinois,,,Tiskilwa,675,America/Chicago
4853511,en,NA,"North America",US,"United States",IA,Iowa,,,Dayton,679,America/Chicago
480876,en,EU,Europe,RU,Russia,ROS,Rostov,,,Tsimlyansk,,Europe/Moscow
3000119,en,EU,Europe,FR,France,89,Yonne,,,"Les Ormes",,Europe/Paris
1 geoname_id locale_code continent_code continent_name country_iso_code country_name subdivision_1_iso_code subdivision_1_name subdivision_2_iso_code subdivision_2_name city_name metro_code time_zone
2 2653810 en EU Europe GB United Kingdom SCT Scotland GLG Glasgow City Cardonald Europe/London
3 2832529 en EU Europe DE Germany RP Rheinland-Pfalz Siefersheim Europe/Berlin
4 2885499 en EU Europe DE Germany MV Mecklenburg-Vorpommern Koerchow Europe/Berlin
5 550870 en EU Europe RU Russia NIZ Nizhegorodskaya Oblast' Khabarskoye Europe/Moscow
6 766583 en EU Europe PL Poland LU Lublin Voivodeship Leczna Europe/Warsaw
7 2608246 en EU Europe AT Austria 1 Burgenland Neuhaus am Klausenbach Europe/Vienna
8 5121765 en NA North America US United States NY New York Ilion 526 America/New_York
9 2935825 en EU Europe DE Germany NW North Rhine-Westphalia Dormagen Europe/Berlin
10 3165189 en EU Europe IT Italy 36 Friuli Venezia Giulia UD Provincia di Udine Tricesimo Europe/Rome
11 4564070 en NA North America PR Puerto Rico Culebra America/Puerto_Rico
12 2993759 en EU Europe FR France U Provence-Alpes-Côte d'Azur 13 Bouches-du-Rhône Miramas-le-Vieux Europe/Paris
13 5861117 en NA North America US United States AK Alaska Dutch Harbor 743 America/Adak
14 4375229 en NA North America US United States MO Missouri Ashland 604 America/Chicago
15 2946980 en EU Europe DE Germany SN Saxony Boehlen Europe/Berlin
16 3156470 en EU Europe NO Norway 02 Akershus Frogner Europe/Oslo
17 3166193 en EU Europe IT Italy 36 Friuli Venezia Giulia GO Provincia di Gorizia Staranzano Europe/Rome
18 4913742 en NA North America US United States IL Illinois Tiskilwa 675 America/Chicago
19 4853511 en NA North America US United States IA Iowa Dayton 679 America/Chicago
20 480876 en EU Europe RU Russia ROS Rostov Tsimlyansk Europe/Moscow
21 3000119 en EU Europe FR France 89 Yonne Les Ormes Europe/Paris

View File

@ -1,21 +0,0 @@
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone
1260633,ja,AS,"アジア",IN,"インド",AP,"アーンドラ・プラデーシュ州",,,,,Asia/Kolkata
4765167,ja,NA,"北アメリカ",US,"アメリカ合衆国",VA,"バージニア州",,,,573,America/New_York
2703330,ja,EU,"ヨーロッパ",SE,"スウェーデン王国",Z,,,,,,Europe/Stockholm
535886,ja,EU,"ヨーロッパ",RU,"ロシア",STA,,,,,,Europe/Moscow
2989001,ja,EU,"ヨーロッパ",FR,"フランス共和国",F,,28,,,,Europe/Paris
3183178,ja,EU,"ヨーロッパ",IT,"イタリア共和国",75,"プッリャ州",BA,,"アルタムーラ",,Europe/Rome
3012956,ja,EU,"ヨーロッパ",FR,"フランス共和国",67,,,,,,Europe/Paris
4189157,ja,NA,"北アメリカ",US,"アメリカ合衆国",GA,"ジョージア州",,,,524,America/New_York
2758965,ja,EU,"ヨーロッパ",NL,"オランダ王国",ZE,,,,,,Europe/Amsterdam
3570412,ja,NA,"北アメリカ",MQ,"マルティニーク島",,,,,,,America/Martinique
3095604,ja,EU,"ヨーロッパ",PL,"ポーランド共和国",MZ,"マゾフシェ県",,,,,Europe/Warsaw
3070865,ja,EU,"ヨーロッパ",CZ,"チェコ共和国",ST,"中央ボヘミア州",,,,,Europe/Prague
2636062,ja,EU,"ヨーロッパ",GB,"イギリス",ENG,"イングランド",SRY,,,,Europe/London
3019338,ja,EU,"ヨーロッパ",FR,"フランス共和国",57,,,,,,Europe/Paris
2865603,ja,EU,"ヨーロッパ",DE,"ドイツ連邦共和国",BY,"バイエルン州",,,"ノイエンマルクト",,Europe/Berlin
2930628,ja,EU,"ヨーロッパ",DE,"ドイツ連邦共和国",HE,,,,,,Europe/Berlin
2976283,ja,EU,"ヨーロッパ",FR,"フランス共和国",01,,,,,,Europe/Paris
4062424,ja,NA,"北アメリカ",US,"アメリカ合衆国",AL,"アラバマ州",,,,575,America/Chicago
4461574,ja,NA,"北アメリカ",US,"アメリカ合衆国",NC,"ノースカロライナ州",,,"コンコード",517,America/New_York
1279945,ja,AS,"アジア",CN,"中国",62,,,,"酒泉市",,Asia/Shanghai
1 geoname_id locale_code continent_code continent_name country_iso_code country_name subdivision_1_iso_code subdivision_1_name subdivision_2_iso_code subdivision_2_name city_name metro_code time_zone
2 1260633 ja AS アジア IN インド AP アーンドラ・プラデーシュ州 Asia/Kolkata
3 4765167 ja NA 北アメリカ US アメリカ合衆国 VA バージニア州 573 America/New_York
4 2703330 ja EU ヨーロッパ SE スウェーデン王国 Z Europe/Stockholm
5 535886 ja EU ヨーロッパ RU ロシア STA Europe/Moscow
6 2989001 ja EU ヨーロッパ FR フランス共和国 F 28 Europe/Paris
7 3183178 ja EU ヨーロッパ IT イタリア共和国 75 プッリャ州 BA アルタムーラ Europe/Rome
8 3012956 ja EU ヨーロッパ FR フランス共和国 67 Europe/Paris
9 4189157 ja NA 北アメリカ US アメリカ合衆国 GA ジョージア州 524 America/New_York
10 2758965 ja EU ヨーロッパ NL オランダ王国 ZE Europe/Amsterdam
11 3570412 ja NA 北アメリカ MQ マルティニーク島 America/Martinique
12 3095604 ja EU ヨーロッパ PL ポーランド共和国 MZ マゾフシェ県 Europe/Warsaw
13 3070865 ja EU ヨーロッパ CZ チェコ共和国 ST 中央ボヘミア州 Europe/Prague
14 2636062 ja EU ヨーロッパ GB イギリス ENG イングランド SRY Europe/London
15 3019338 ja EU ヨーロッパ FR フランス共和国 57 Europe/Paris
16 2865603 ja EU ヨーロッパ DE ドイツ連邦共和国 BY バイエルン州 ノイエンマルクト Europe/Berlin
17 2930628 ja EU ヨーロッパ DE ドイツ連邦共和国 HE Europe/Berlin
18 2976283 ja EU ヨーロッパ FR フランス共和国 01 Europe/Paris
19 4062424 ja NA 北アメリカ US アメリカ合衆国 AL アラバマ州 575 America/Chicago
20 4461574 ja NA 北アメリカ US アメリカ合衆国 NC ノースカロライナ州 コンコード 517 America/New_York
21 1279945 ja AS アジア CN 中国 62 酒泉市 Asia/Shanghai

View File

@ -1,52 +0,0 @@
name: geolite-csv
version: 0.2
synopsis: Geolite CSV Parser
description: Please see README.md
homepage: https://github.com/andrewthad/colonnade
license: BSD3
license-file: LICENSE
author: Andrew Martin
maintainer: andrew.thaddeus@gmail.com
copyright: 2016 Andrew Martin
category: web
build-type: Simple
-- extra-source-files:
cabal-version: >=1.10
library
hs-source-dirs: src
exposed-modules:
Geolite.Types
Geolite.Csv
build-depends:
base >= 4.7 && < 5
, colonnade
, siphon
, ip >= 0.8.4
, text
, pipes
default-language: Haskell2010
test-suite geolite-csv-test
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Spec.hs
build-depends:
base
, geolite-csv
, siphon
, colonnade
, test-framework
, text
, pipes
, HUnit
, test-framework-hunit
, pipes-bytestring
, pipes-text
, directory
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010
source-repository head
type: git
location: https://github.com/andrewthad/colonnade

View File

@ -1,48 +0,0 @@
#!/bin/bash
set -e
if [ "$#" -ne 1 ]; then
echo "Usage: scripts/hackage-docs.sh HACKAGE_USER"
exit 1
fi
user=$1
cabal_file=$(find . -maxdepth 1 -name "*.cabal" -print -quit)
if [ ! -f "$cabal_file" ]; then
echo "Run this script in the top-level package directory"
exit 1
fi
pkg=$(awk -F ":[[:space:]]*" 'tolower($1)=="name" { print $2 }' < "$cabal_file")
ver=$(awk -F ":[[:space:]]*" 'tolower($1)=="version" { print $2 }' < "$cabal_file")
if [ -z "$pkg" ]; then
echo "Unable to determine package name"
exit 1
fi
if [ -z "$ver" ]; then
echo "Unable to determine package version"
exit 1
fi
echo "Detected package: $pkg-$ver"
dir=$(mktemp -d build-docs.XXXXXX)
trap 'rm -r "$dir"' EXIT
# cabal haddock --hoogle --hyperlink-source --html-location='/package/$pkg-$version/docs' --contents-location='/package/$pkg-$version'
stack haddock
cp -R .stack-work/dist/x86_64-linux/Cabal-1.22.5.0/doc/html/$pkg/ $dir/$pkg-$ver-docs
# /home/andrew/.stack/snapshots/x86_64-linux/lts-5.17/7.10.3/doc/index.html
tar cvz -C $dir --format=ustar -f $dir/$pkg-$ver-docs.tar.gz $pkg-$ver-docs
curl -X PUT \
-H 'Content-Type: application/x-tar' \
-H 'Content-Encoding: gzip' \
-u "$user" \
--data-binary "@$dir/$pkg-$ver-docs.tar.gz" \
"https://hackage.haskell.org/package/$pkg-$ver/docs"

View File

@ -1,35 +0,0 @@
#!/bin/bash
set -e
current_dir="${PWD##*/}"
echo "Current directory is: $current_dir"
if [ "$current_dir" = "colonnade" ]
then
cd ./geolite-csv
fi
new_current_dir="${PWD##*/}"
if [ "$new_current_dir" != "geolite-csv" ]
then
echo "Not currently in the geolite project directory. Exiting."
exit 1
fi
mkdir -p ./data/large
cd ./data/large
rm -f *.zip
rm -rf GeoLite2-*
curl 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City-CSV.zip' > archive.zip
unzip archive.zip -d ./
cd GeoLite2-City-CSV*
mv *.csv ../
cd ../
rm -rf GeoLite2-City-CSV*
rm archive.zip

View File

@ -1,59 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Geolite.Csv where
import Data.Text (Text)
import Pipes (Pipe)
import Colonnade.Types
import Geolite.Types
import qualified Data.Text as Text
import qualified Net.IPv4.Range.Text as IPv4RangeText
import qualified Data.Text.Read as TextRead
import qualified Siphon.Decoding as SD
import qualified Siphon.Content as SC
import qualified Colonnade.Decoding.Text as CDT
import qualified Colonnade.Decoding as CD
cities :: Monad m => Pipe Text City m (DecodingRowError Headed Text)
cities = SD.headedPipe SC.text decodingCity
blocks :: Monad m => Pipe Text Block m (DecodingRowError Headed Text)
blocks = SD.headedPipe SC.text decodingBlock
decodingCity :: Decoding Headed Text City
decodingCity = City
<$> fmap GeonameId (CD.headed "geoname_id" CDT.int)
<*> CD.headed "locale_code" CDT.text
<*> CD.headed "continent_code" CDT.text
<*> CD.headed "continent_name" CDT.text
<*> CD.headed "country_iso_code" CDT.text
<*> CD.headed "country_name" CDT.text
<*> CD.headed "subdivision_1_iso_code" CDT.text
<*> CD.headed "subdivision_1_name" CDT.text
<*> CD.headed "subdivision_2_iso_code" CDT.text
<*> CD.headed "subdivision_2_name" CDT.text
<*> CD.headed "city_name" CDT.text
<*> CD.headed "metro_code" (CDT.optional CDT.int)
<*> CD.headed "time_zone" CDT.text
decodingBlock :: Decoding Headed Text Block
decodingBlock = Block
<$> CD.headed "network" IPv4RangeText.decodeEither
<*> CD.headed "geoname_id"
(CDT.optional $ CDT.map GeonameId CDT.int)
<*> CD.headed "registered_country_geoname_id"
(CDT.optional $ CDT.map GeonameId CDT.int)
<*> CD.headed "represented_country_geoname_id"
(CDT.optional $ CDT.map GeonameId CDT.int)
<*> CD.headed "is_anonymous_proxy" (CDT.trueFalse "1" "0")
<*> CD.headed "is_satellite_provider" (CDT.trueFalse "1" "0")
<*> CD.headed "postal_code" CDT.text
<*> CD.headed "latitude"
(CDT.optional $ CDT.fromReader TextRead.rational)
<*> CD.headed "longitude"
(CDT.optional $ CDT.fromReader TextRead.rational)
<*> CD.headed "accuracy_radius"
(CDT.optional CDT.int)

View File

@ -1,43 +0,0 @@
module Geolite.Types where
import Net.Types (IPv4Range)
import Data.Text (Text)
import Data.Fixed
data E4
instance HasResolution E4 where
resolution _ = 4
newtype GeonameId = GeonameId { getGeonameId :: Int }
deriving (Show,Read,Eq,Ord)
data City = City
{ cityGeonameId :: GeonameId
, cityLocaleCode :: Text
, cityContinentCode :: Text
, cityContinentName :: Text
, cityCountryIsoCode :: Text
, cityCountryName :: Text
, citySubdivision1IsoCode :: Text
, citySubdivision1Name :: Text
, citySubdivision2IsoCode :: Text
, citySubdivision2Name :: Text
, cityName :: Text
, cityMetroCode :: Maybe Int
, cityTimeZone :: Text
} deriving (Show,Read,Eq,Ord)
data Block = Block
{ blockNetwork :: IPv4Range
, blockGeonameId :: Maybe GeonameId
, blockRegisteredCountryGeonameId :: Maybe GeonameId
, blockRepresentedCountryGeonameId :: Maybe GeonameId
, blockIsAnonymousProxy :: Bool
, blockIsSatelliteProvider :: Bool
, blockPostalCode :: Text
, blockLatitude :: Maybe (Fixed E4)
, blockLongitude :: Maybe (Fixed E4)
, blockAccuracyRadius :: Maybe Int
} deriving (Show,Read,Eq,Ord)

View File

@ -1,91 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Test.HUnit (Assertion,(@?=),assertBool,assertFailure)
import Test.Framework (defaultMainWithOpts, interpretArgsOrExit,
testGroup, Test)
import Test.Framework.Providers.HUnit (testCase)
import Test.Framework.Runners.TestPattern (parseTestPattern)
import Test.Framework.Runners.Options (RunnerOptions'(..))
import Geolite.Csv (cities,blocks)
import Data.Text (Text)
import Colonnade.Types
import Siphon.Types
import Data.Functor.Identity
import Control.Monad (unless)
import System.Environment (getArgs)
import System.Directory (doesDirectoryExist)
import System.IO (withFile,IOMode(ReadMode))
import qualified Data.Text as Text
import qualified Pipes.Prelude as Pipes
import qualified Pipes.ByteString as PB
import qualified Pipes.Text.Encoding as PT
import qualified Siphon.Decoding as SD
import qualified Colonnade.Decoding as Decoding
import Pipes
------------------------------------------------
-- The default behavior of this test suite is to
-- test the CSV decoders against small samples of
-- the GeoLite2 databases. These small samples are
-- included as part of this repository. If you give
-- this test suite an argument named "large", it
-- will run against the full CSVs, which are around
-- 350MB. These are not included
-- as part of the repository, so they need to be
-- downloaded. The script found in
-- scripts/load-full-databases will download the full
-- archive, decompress it, and move the files to
-- the appropriate directory for this test suite
-- to run on them.
-----------------------------------------------
main :: IO ()
main = do
xs <- getArgs
ropts' <- interpretArgsOrExit xs
let ropts = ropts'
{ ropt_test_patterns = case ropt_test_patterns ropts' of
Nothing -> Just [parseTestPattern "small"]
Just xs -> Just xs
}
defaultMainWithOpts tests ropts
tests :: [Test]
tests = flip concatMap ["small","large"] $ \size ->
[ testGroup size
[ testCase "Network Blocks" $ streamFileWith
("data/" ++ size ++ "/GeoLite2-City-Blocks-IPv4.csv")
blocks
, testCase "English City Locations" $ streamFileWith
("data/" ++ size ++ "/GeoLite2-City-Locations-en.csv")
cities
, testCase "Japanese City Locations" $ streamFileWith
("data/" ++ size ++ "/GeoLite2-City-Locations-ja.csv")
cities
]
]
streamFileWith ::
String
-> Pipe Text a IO (DecodingRowError Headed Text)
-> Assertion
streamFileWith filename decodingPipe = do
r <- withFile filename ReadMode $ \h -> runEffect $
fmap (SD.convertDecodeError "utf-8") (PT.decode (PT.utf8 . PT.eof) $ PB.fromHandle h)
>-> fmap Just decodingPipe
>-> Pipes.drain
case r of
Nothing -> assertBool "impossible" True
Just err -> assertFailure (Decoding.prettyError Text.unpack err)
-- let dirPiece = case xs of
-- ["full"] -> "large/"
-- _ -> "small/"
-- fullDirName = "data/" ++ dirPiece
-- errMsg = concat
-- [ "The "
-- , fullDirName
-- , " directory does not exist in the geolite project"
-- ]

View File

@ -1,30 +0,0 @@
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.

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,29 +0,0 @@
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 && < 2.1
, vector >= 0.10 && < 0.14
default-language: Haskell2010
source-repository head
type: git
location: https://github.com/andrewthad/colonnade

View File

@ -1,292 +0,0 @@
{-# 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 = (<>)
-- | 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

View File

@ -1,4 +0,0 @@
packages: ./colonnade
./blaze-colonnade
./lucid-colonnade
./yesod-colonnade

View File

@ -1,4 +0,0 @@
packages: ./colonnade
./blaze-colonnade
./lucid-colonnade
./yesod-colonnade

View File

@ -1,3 +0,0 @@
packages: ./colonnade
./blaze-colonnade
./lucid-colonnade

View File

@ -1,9 +0,0 @@
# Revision history for siphon
## 0.8.2.0 -- 2022-??-??
* Add
## 0.8.1.2 -- 2021-10-25
* Correct handling of CRLF.

View File

@ -1,30 +0,0 @@
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.

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,48 +0,0 @@
#!/bin/bash
set -e
if [ "$#" -ne 1 ]; then
echo "Usage: scripts/hackage-docs.sh HACKAGE_USER"
exit 1
fi
user=$1
cabal_file=$(find . -maxdepth 1 -name "*.cabal" -print -quit)
if [ ! -f "$cabal_file" ]; then
echo "Run this script in the top-level package directory"
exit 1
fi
pkg=$(awk -F ":[[:space:]]*" 'tolower($1)=="name" { print $2 }' < "$cabal_file")
ver=$(awk -F ":[[:space:]]*" 'tolower($1)=="version" { print $2 }' < "$cabal_file")
if [ -z "$pkg" ]; then
echo "Unable to determine package name"
exit 1
fi
if [ -z "$ver" ]; then
echo "Unable to determine package version"
exit 1
fi
echo "Detected package: $pkg-$ver"
dir=$(mktemp -d build-docs.XXXXXX)
trap 'rm -r "$dir"' EXIT
# cabal haddock --hoogle --hyperlink-source --html-location='/package/$pkg-$version/docs' --contents-location='/package/$pkg-$version'
stack haddock
cp -R .stack-work/dist/x86_64-linux/Cabal-1.22.5.0/doc/html/$pkg/ $dir/$pkg-$ver-docs
# /home/andrew/.stack/snapshots/x86_64-linux/lts-5.17/7.10.3/doc/index.html
tar cvz -C $dir --format=ustar -f $dir/$pkg-$ver-docs.tar.gz $pkg-$ver-docs
curl -X PUT \
-H 'Content-Type: application/x-tar' \
-H 'Content-Encoding: gzip' \
-u "$user" \
--data-binary "@$dir/$pkg-$ver-docs.tar.gz" \
"https://hackage.haskell.org/package/$pkg-$ver/docs"

View File

@ -1,58 +0,0 @@
cabal-version: 3.0
name: siphon
version: 0.8.2.0
synopsis: Encode and decode CSV files
description: Please see README.md
homepage: https://github.com/andrewthad/colonnade#readme
license: BSD-3-Clause
license-file: LICENSE
author: Andrew Martin
maintainer: andrew.thaddeus@gmail.com
copyright: 2016 Andrew Martin
category: web
build-type: Simple
extra-source-files: CHANGELOG.md
library
hs-source-dirs: src
exposed-modules:
Siphon
Siphon.Types
build-depends:
base >= 4.8 && < 5
, colonnade >= 1.2 && < 1.3
, text >= 1.0 && < 2.1
, bytestring
, vector
, streaming >= 0.1.4 && < 0.3
, attoparsec
, transformers >= 0.4.2 && < 0.8
, semigroups >= 0.18.2 && < 0.21
default-language: Haskell2010
test-suite test
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Test.hs
build-depends:
base
, HUnit
, QuickCheck
, bytestring
, colonnade
, contravariant
, either
, pipes
, profunctors
, siphon
, streaming
, test-framework
, test-framework-hunit
, test-framework-quickcheck2
, text
, vector
default-language: Haskell2010
source-repository head
type: git
location: https://github.com/andrewthad/colonnade

View File

@ -1,791 +0,0 @@
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -Wall -fno-warn-unused-imports #-}
-- | 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
, decodeHeadedCsvUtf8
, decodeIndexedCsvUtf8
-- * Build Siphon
, headed
, headless
, indexed
-- * Types
, Siphon
, SiphonError(..)
, Indexed(..)
-- * For Testing
, headedToIndexed
-- * Utility
, humanizeSiphonError
, eqSiphonHeaders
, showSiphonHeaders
-- * Imports
-- $setup
) where
import Siphon.Types
import Data.Monoid
import Control.Applicative
import Control.Monad
import Data.Functor.Classes (Eq1,Show1,liftEq,showsPrec1)
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)
-- | Backwards-compatibility alias for 'decodeHeadedCsvUtf8'.
decodeCsvUtf8 :: Monad m
=> Siphon CE.Headed ByteString a
-> Stream (Of ByteString) m () -- ^ encoded csv
-> Stream (Of a) m (Maybe SiphonError)
decodeCsvUtf8 = decodeHeadedCsvUtf8
-- | Decode a CSV whose first row is contains headers identify each column.
decodeHeadedCsvUtf8 :: Monad m
=> Siphon CE.Headed ByteString a
-> Stream (Of ByteString) m () -- ^ encoded csv
-> Stream (Of a) m (Maybe SiphonError)
decodeHeadedCsvUtf8 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
-- | Decode a CSV without a header.
decodeIndexedCsvUtf8 :: Monad m
=> Int -- ^ How many columns are there? This number should be greater than any indices referenced by the scheme.
-> Siphon Indexed ByteString a
-> Stream (Of ByteString) m () -- ^ encoded csv
-> Stream (Of a) m (Maybe SiphonError)
decodeIndexedCsvUtf8 !requiredLength ixedSiphon s1 = do
consumeBodyUtf8 0 requiredLength ixedSiphon s1
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
-- | 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 Indexed 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 Indexed 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)
| 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 (Indexed ix) 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 -> A.anyWord8 >> return TrailCharNewline
| b == cr -> do
_ <- A.anyWord8
_ <- A.word8 newline
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 -> A.anyWord8 >> return (bs,TrailCharNewline)
| b == cr -> do
_ <- A.anyWord8
_ <- A.word8 newline
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
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 Indexed 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 Indexed 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 (len - 1) 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
-- This is built in reverse and then reversed by reverseVectorStrictList
-- when converting to a vector.
data StrictList a = StrictListNil | StrictListCons !a !(StrictList a)
-- | This function uses 'unsafeIndex' to access
-- elements of the 'Vector'.
uncheckedRunWithRow ::
(c -> T.Text)
-> Int
-> Siphon Indexed 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 Indexed c a
-> Vector c
-> Either (Vector CellError) a
uncheckedRun toStr dc v = getEitherWrap (go dc)
where
go :: forall b.
Siphon Indexed c b
-> EitherWrap (Vector CellError) b
go (SiphonPure b) = EitherWrap (Right b)
go (SiphonAp (Indexed 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)
-- | 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)
eqSiphonHeaders :: (Eq1 f, Eq c) => Siphon f c a -> Siphon f c b -> Bool
eqSiphonHeaders (SiphonPure _) (SiphonPure _) = True
eqSiphonHeaders (SiphonAp h0 _ s0) (SiphonAp h1 _ s1) =
liftEq (==) h0 h1 && eqSiphonHeaders s0 s1
eqSiphonHeaders _ _ = False
showSiphonHeaders :: (Show1 f, Show c) => Siphon f c a -> String
showSiphonHeaders (SiphonPure _) = ""
showSiphonHeaders (SiphonAp h0 _ s0) = showsPrec1 10 h0 (" :> " ++ showSiphonHeaders s0)
-- $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}

View File

@ -1 +0,0 @@
module Siphon.ByteString.Char8 where

View File

@ -1,8 +0,0 @@
module Siphon.Content
( byteStringChar8
, text
) where
import Siphon.Internal (byteStringChar8)
import Siphon.Internal.Text (text)

View File

@ -1,336 +0,0 @@
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveFunctor #-}
module Siphon.Decoding
( mkParseError
, headlessPipe
, indexedPipe
, headedPipe
, consumeGeneral
, pipeGeneral
, convertDecodeError
) where
import Siphon.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 Data.Attoparsec.ByteString as AttoByteString
import qualified Data.ByteString.Char8 as ByteString
import qualified Data.Attoparsec.Types as Atto
mkParseError :: Int -> [String] -> String -> DecolonnadeRowError f content
mkParseError i ctxs msg = id
$ DecolonnadeRowError i
$ RowErrorParse $ concat
[ "Contexts: ["
, concat ctxs
, "], Error Message: ["
, msg
, "]"
]
-- | This is a convenience function for working with @pipes-text@.
-- 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 (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
-> Decolonnade Headless c a
-> Pipe c a m (DecolonnadeRowError Headless c)
headlessPipe sd decoding = uncheckedPipe requiredLength 0 sd indexedDecoding Nothing
where
indexedDecoding = headlessToIndexed decoding
requiredLength = decLength indexedDecoding
indexedPipe :: Monad m
=> Siphon 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 = maxIndex decoding
vlen = Vector.length firstRow
in if vlen < req
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
headedPipe :: (Monad m, Eq c)
=> Siphon 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 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
uncheckedPipe :: Monad m
=> Int -- ^ expected length of each row
-> Int -- ^ index of first row, usually zero or one
-> Siphon c
-> Decolonnade (Indexed f) c a
-> Maybe 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 $ DecolonnadeRowError rowIx
$ RowErrorSize requiredLength vlen
else uncheckedRunWithRow rowIx d v
consumeGeneral :: Monad m
=> Int
-> Siphon c
-> (Int -> [String] -> String -> e)
-> Consumer' c m (Either e (Vector c, Maybe c))
consumeGeneral ix (Siphon _ _ parse isNull) wrapParseError = do
c <- awaitSkip isNull
handleResult (parse c)
where
go k = do
c <- awaitSkip isNull
handleResult (k c)
handleResult r = case r of
Atto.Fail _ ctxs msg -> return $ Left
$ wrapParseError ix ctxs msg
Atto.Done c v ->
let mcontent = if isNull c
then Nothing
else Just c
in return (Right (v,mcontent))
Atto.Partial k -> go k
pipeGeneral :: Monad m
=> Int -- ^ index of first row, usually zero or one
-> Siphon c
-> (Int -> [String] -> String -> e)
-> (Int -> Vector c -> Either e a)
-> Maybe c -- ^ leftovers that should be handled first
-> Pipe c a m e
pipeGeneral initIx (Siphon _ _ parse isNull) wrapParseError decodeRow mleftovers =
case mleftovers of
Nothing -> go1 initIx
Just leftovers -> handleResult initIx (parse leftovers)
where
go1 !ix = do
c1 <- awaitSkip isNull
handleResult ix (parse c1)
go2 !ix c1 = handleResult ix (parse c1)
go3 !ix k = do
c1 <- awaitSkip isNull
handleResult ix (k c1)
handleResult !ix r = case r of
Atto.Fail _ ctxs msg -> return $ wrapParseError ix ctxs msg
Atto.Done c1 v -> do
case decodeRow ix v of
Left err -> return err
Right r -> do
yield r
let ixNext = ix + 1
if isNull c1 then go1 ixNext else go2 ixNext c1
Atto.Partial k -> go3 ix k
awaitSkip :: Monad m
=> (a -> Bool)
-> Consumer' a m a
awaitSkip f = go where
go = do
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)

View File

@ -1,30 +0,0 @@
module Siphon.Encoding where
import Siphon.Types
import Colonnade (Colonnade,Headed)
import Pipes (Pipe,yield)
import qualified Pipes.Prelude as Pipes
import qualified Colonnade.Encode as E
row :: Siphon c -> Colonnade f a c -> a -> c
row (Siphon escape intercalate _ _) e =
intercalate . E.row escape e
header :: Siphon c -> Colonnade Headed a c -> c
header (Siphon escape intercalate _ _) e =
intercalate (E.header escape e)
pipe :: Monad m
=> Siphon c
-> Colonnade f a c
-> Pipe a c m x
pipe siphon encoding = Pipes.map (row siphon encoding)
headedPipe :: Monad m
=> Siphon c
-> Colonnade Headed a c
-> Pipe a c m x
headedPipe siphon encoding = do
yield (header siphon encoding)
pipe siphon encoding

View File

@ -1,214 +0,0 @@
{-# LANGUAGE BangPatterns #-}
-- | A CSV parser. The parser defined here is RFC 4180 compliant, with
-- the following extensions:
--
-- * Empty lines are ignored.
--
-- * Non-escaped fields may contain any characters except
-- double-quotes, commas, carriage returns, and newlines.
--
-- * Escaped fields may contain any characters (but double-quotes
-- need to be escaped).
--
-- The functions in this module can be used to implement e.g. a
-- resumable parser that is fed input incrementally.
module Siphon.Internal where
import Siphon.Types
import Data.ByteString.Builder (toLazyByteString,byteString)
import qualified Data.ByteString.Char8 as BC8
import Control.Applicative (optional)
import Data.Attoparsec.ByteString.Char8 (char, endOfInput, string)
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 as T
import Data.Word (Word8)
import Data.Vector (Vector)
import Data.ByteString (ByteString)
import Data.Coerce (coerce)
import Siphon.Types
import Control.Applicative
import Data.Monoid
byteStringChar8 :: Siphon ByteString
byteStringChar8 = Siphon
escape
encodeRow
(A.parse (row comma))
B.null
encodeRow :: Vector (Escaped ByteString) -> ByteString
encodeRow = id
. flip B.append (B.singleton newline)
. B.intercalate (B.singleton comma)
. V.toList
. coerce
escape :: ByteString -> Escaped ByteString
escape t = case B.find (\c -> c == newline || c == cr || c == comma || c == doubleQuote) t of
Nothing -> Escaped t
Just _ -> escapeAlways 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
-- | Specialized version of 'sepBy1'' which is faster due to not
-- accepting an arbitrary separator.
sepByDelim1' :: AL.Parser a
-> Word8 -- ^ Field delimiter
-> AL.Parser [a]
sepByDelim1' p !delim = liftM2' (:) p loop
where
loop = do
mb <- A.peekWord8
case mb of
Just b | b == delim -> liftM2' (:) (A.anyWord8 *> p) loop
_ -> pure []
{-# INLINE sepByDelim1' #-}
-- | Specialized version of 'sepBy1'' which is faster due to not
-- accepting an arbitrary separator.
sepByEndOfLine1' :: AL.Parser a
-> AL.Parser [a]
sepByEndOfLine1' p = liftM2' (:) p loop
where
loop = do
mb <- A.peekWord8
case mb of
Just b | b == cr ->
liftM2' (:) (A.anyWord8 *> A.word8 newline *> p) loop
| b == newline ->
liftM2' (:) (A.anyWord8 *> p) loop
_ -> pure []
{-# INLINE sepByEndOfLine1' #-}
-- | 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.
field :: Word8 -> AL.Parser 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 -> escapedField
_ -> unescapedField delim
{-# INLINE field #-}
escapedField :: AL.Parser S.ByteString
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)
if doubleQuote `S.elem` s
then case Z.parse unescape s of
Right r -> return r
Left err -> fail err
else return s
unescapedField :: Word8 -> AL.Parser S.ByteString
unescapedField !delim = A.takeWhile (\ c -> c /= doubleQuote &&
c /= newline &&
c /= delim &&
c /= cr)
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
-- | A strict version of 'Data.Functor.<$>' for monads.
(<$!>) :: Monad m => (a -> b) -> m a -> m b
f <$!> m = do
a <- m
return $! f a
{-# INLINE (<$!>) #-}
infixl 4 <$!>
-- | 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))
-- | A version of 'liftM2' that is strict in the result of its first
-- action.
liftM2' :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c
liftM2' f a b = do
!x <- a
y <- b
return (f x y)
{-# INLINE liftM2' #-}
-- | Match either a single newline character @\'\\n\'@, or a carriage
-- return followed by a newline character @\"\\r\\n\"@, or a single
-- carriage return @\'\\r\'@.
endOfLine :: A.Parser ()
endOfLine = (A.word8 newline *> return ()) <|> (string (BC8.pack "\r\n") *> return ()) <|> (A.word8 cr *> return ())
{-# INLINE endOfLine #-}
doubleQuote, newline, cr, comma :: Word8
doubleQuote = 34
newline = 10
cr = 13
comma = 44

View File

@ -1,189 +0,0 @@
{-# LANGUAGE BangPatterns #-}
module Siphon.Internal.Text where
import Siphon.Types
import Control.Applicative (optional)
import Data.Attoparsec.Text (char, endOfInput, string)
import qualified Data.Attoparsec.Text as A
import qualified Data.Attoparsec.Text.Lazy as AL
import qualified Data.Attoparsec.Zepto as Z
import qualified Data.Text as T
import qualified Data.Text as Text
import qualified Data.Vector as V
import qualified Data.Text.Lazy as LText
import qualified Data.Text.Lazy.Builder as Builder
import Data.Text.Lazy.Builder (Builder)
import Data.Word (Word8)
import Data.Vector (Vector)
import Data.Text (Text)
import Data.Coerce (coerce)
import Siphon.Types
import Control.Applicative
import Data.Monoid
text :: Siphon Text
text = Siphon
escape
encodeRow
(A.parse (row comma))
Text.null
encodeRow :: Vector (Escaped Text) -> Text
encodeRow = id
. flip Text.append (Text.singleton newline)
. Text.intercalate (Text.singleton comma)
. V.toList
. coerce
escape :: Text -> Escaped Text
escape t = case Text.find (\c -> c == newline || c == cr || c == comma || c == doubleQuote) t of
Nothing -> Escaped t
Just _ -> escapeAlways 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 text by writing to a buffer directly.
escapeAlways :: Text -> Escaped Text
escapeAlways t = Escaped $ Text.concat
[ textDoubleQuote
, Text.replace textDoubleQuote (Text.pack [doubleQuote,doubleQuote]) t
, textDoubleQuote
]
-- | Specialized version of 'sepBy1'' which is faster due to not
-- accepting an arbitrary separator.
sepByDelim1' :: A.Parser a
-> Char -- ^ Field delimiter
-> A.Parser [a]
sepByDelim1' p !delim = liftM2' (:) p loop
where
loop = do
mb <- A.peekChar
case mb of
Just b | b == delim -> liftM2' (:) (A.anyChar *> p) loop
_ -> pure []
{-# INLINE sepByDelim1' #-}
-- | Specialized version of 'sepBy1'' which is faster due to not
-- accepting an arbitrary separator.
sepByEndOfLine1' :: A.Parser a
-> A.Parser [a]
sepByEndOfLine1' p = liftM2' (:) p loop
where
loop = do
mb <- A.peekChar
case mb of
Just b | b == cr ->
liftM2' (:) (A.anyChar *> A.char newline *> p) loop
| b == newline ->
liftM2' (:) (A.anyChar *> p) loop
_ -> pure []
{-# INLINE sepByEndOfLine1' #-}
-- | 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 :: Char -- ^ Field delimiter
-> A.Parser (Vector Text)
row !delim = rowNoNewline delim <* endOfLine
{-# INLINE row #-}
rowNoNewline :: Char -- ^ Field delimiter
-> A.Parser (Vector Text)
rowNoNewline !delim = V.fromList <$!> field delim `sepByDelim1'` delim
{-# INLINE rowNoNewline #-}
-- | Parse a field. The field may be in either the escaped or
-- non-escaped format. The return value is unescaped.
field :: Char -> A.Parser Text
field !delim = do
mb <- A.peekChar
-- 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 -> escapedField
_ -> unescapedField delim
{-# INLINE field #-}
escapedField :: A.Parser Text
escapedField = do
_ <- dquote -- This can probably be replaced with anyChar
b <- escapedFieldInner mempty
return (LText.toStrict (Builder.toLazyText b))
escapedFieldInner :: Builder -> A.Parser Builder
escapedFieldInner b = do
t <- A.takeTill (== doubleQuote)
_ <- A.anyChar -- this will always be a double quote
c <- A.peekChar'
if c == doubleQuote
then do
_ <- A.anyChar -- this will always be a double quote
escapedFieldInner (b `mappend` Builder.fromText t `mappend` Builder.fromText textDoubleQuote)
else return (b `mappend` Builder.fromText t)
unescapedField :: Char -> A.Parser Text
unescapedField !delim = A.takeWhile (\ c -> c /= doubleQuote &&
c /= newline &&
c /= delim &&
c /= cr)
dquote :: A.Parser Char
dquote = char doubleQuote
unescape :: A.Parser Text
unescape = (LText.toStrict . Builder.toLazyText) <$!> go mempty where
go acc = do
h <- A.takeWhile (/= doubleQuote)
let rest = do
c0 <- A.anyChar
c1 <- A.anyChar
if (c0 == doubleQuote && c1 == doubleQuote)
then go (acc `mappend` Builder.fromText h `mappend` Builder.fromText textDoubleQuote)
else fail "invalid CSV escape sequence"
done <- A.atEnd
if done
then return (acc `mappend` Builder.fromText h)
else rest
-- | A strict version of 'Data.Functor.<$>' for monads.
(<$!>) :: Monad m => (a -> b) -> m a -> m b
f <$!> m = do
a <- m
return $! f a
{-# INLINE (<$!>) #-}
infixl 4 <$!>
-- | A version of 'liftM2' that is strict in the result of its first
-- action.
liftM2' :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c
liftM2' f a b = do
!x <- a
y <- b
return (f x y)
{-# INLINE liftM2' #-}
-- | Match either a single newline character @\'\\n\'@, or a carriage
-- return followed by a newline character @\"\\r\\n\"@, or a single
-- carriage return @\'\\r\'@.
endOfLine :: A.Parser ()
endOfLine = (A.char newline *> return ()) <|> (string (Text.pack "\r\n") *> return ()) <|> (A.char cr *> return ())
{-# INLINE endOfLine #-}
textDoubleQuote :: Text
textDoubleQuote = Text.singleton doubleQuote
doubleQuote, newline, cr, comma :: Char
doubleQuote = '\"'
newline = '\n'
cr = '\r'
comma = ','

View File

@ -1,33 +0,0 @@
module Siphon.Text where
import Siphon.Types
import Data.Text (Text)
import Data.Vector (Vector)
import Data.Coerce (coerce)
import qualified Data.Text as Text
import qualified Data.Vector as Vector
siphon :: Siphon Text
siphon = Siphon escape encodeRow
(error "siphon: uhoent") (error "siphon: uheokj")
encodeRow :: Vector (Escaped Text) -> Text
encodeRow = id
. Text.intercalate (Text.singleton ',')
. Vector.toList
. coerce
escape :: Text -> Escaped Text
escape t = case Text.find (\c -> c == '\n' || c == ',' || c == '"') t of
Nothing -> Escaped t
Just _ -> escapeAlways t
escapeAlways :: Text -> Escaped Text
escapeAlways t = Escaped $ Text.concat
[ Text.singleton '"'
, Text.replace (Text.pack "\"") (Text.pack "\"\"") t
, Text.singleton '"'
]

View File

@ -1,84 +0,0 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -Wall -Werror #-}
module Siphon.Types
( Siphon(..)
, Indexed(..)
, SiphonError(..)
, RowError(..)
, CellError(..)
) where
import Data.Vector (Vector)
import Control.Exception (Exception)
import Data.Text (Text)
import Data.Functor.Classes (Eq1,Show1,liftEq,liftShowsPrec)
data CellError = CellError
{ cellErrorColumn :: !Int
, cellErrorContent :: !Text
} deriving (Show,Read,Eq)
newtype Indexed a = Indexed
{ indexedIndex :: Int
} deriving (Eq,Ord,Functor,Show,Read)
instance Show1 Indexed where
liftShowsPrec _ _ p (Indexed i) s = showsPrec p i s
instance Eq1 Indexed where
liftEq _ (Indexed i) (Indexed j) = i == j
data SiphonError = SiphonError
{ siphonErrorRow :: !Int
, siphonErrorCause :: !RowError
} deriving (Show,Read,Eq)
instance Exception SiphonError
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)
-- | 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
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)

View File

@ -1,388 +0,0 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
module Main (main) where
import Colonnade (headed,headless,Colonnade,Headed,Headless)
import Control.Exception
import Data.ByteString (ByteString)
import Data.Char (ord)
import Data.Either.Combinators
import Data.Functor.Contravariant (contramap)
import Data.Functor.Contravariant.Divisible (divided,conquered)
import Data.Functor.Identity
import Data.Profunctor (lmap)
import Data.Text (Text)
import Data.Word (Word8)
import Debug.Trace
import GHC.Generics (Generic)
import Siphon.Types
import Streaming (Stream,Of(..))
import Test.Framework (defaultMain, testGroup, Test)
import Test.Framework.Providers.HUnit (testCase)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.HUnit (Assertion,(@?=))
import Test.QuickCheck (Gen, Arbitrary(..), choose, elements, Property)
import Test.QuickCheck.Property (Result, succeeded, exception)
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 Data.Vector as Vector
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
tests :: [Test]
tests =
[ testGroup "ByteString encode/decode"
[ 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 [(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)
, testCase "Headed Decoding (escaped characters, character per chunk, CRLF)"
$ ( runIdentity . SMP.toList )
( S.decodeCsvUtf8 decodingF
( mapM_ (SMP.yield . BC8.singleton) $ concat
[ "name\r\n"
, "drew\r\n"
, "\"martin, drew\"\r\n"
]
)
) @?= (["drew","martin, drew"] :> Nothing)
, testCase "headedToIndexed" $
let actual = S.headedToIndexed id (Vector.fromList ["letter","boolean","number"]) decodingG
in case actual of
Left e -> fail "headedToIndexed failed"
Right actualInner ->
let expected = SiphonAp (Indexed 2 :: Indexed Text) (\_ -> Nothing)
$ SiphonAp (Indexed 0 :: Indexed Text) (\_ -> Nothing)
$ SiphonAp (Indexed 1 :: Indexed Text) (\_ -> Nothing)
$ SiphonPure (\_ _ _ -> ())
in case S.eqSiphonHeaders actualInner expected of
True -> pure ()
False -> fail $
"Expected " ++
S.showSiphonHeaders expected ++
" but got " ++
S.showSiphonHeaders actualInner
, testCase "Indexed Decoding (int,char,bool)"
$ ( runIdentity . SMP.toList )
( S.decodeIndexedCsvUtf8 3 indexedDecodingB
( mapM_ (SMP.yield . BC8.singleton) $ concat
[ "244,z,true\n"
]
)
) @?= ([(244,intToWord8 (ord 'z'),True)] :> Nothing)
, testProperty "Headed Isomorphism (int,char,bool)"
$ 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)
instance Arbitrary Foo where
arbitrary = elements [minBound..maxBound]
fooToString :: Foo -> String
fooToString x = case x of
FooA -> "Simple"
FooB -> "With,Escaped\nChars"
FooC -> "More\"Escaped,\"\"Chars"
encodeFoo :: (String -> c) -> Foo -> c
encodeFoo f = f . fooToString
fooFromString :: String -> Maybe Foo
fooFromString x = case x of
"Simple" -> Just FooA
"With,Escaped\nChars" -> Just FooB
"More\"Escaped,\"\"Chars" -> Just FooC
_ -> Nothing
decodeFoo :: (c -> String) -> c -> Maybe Foo
decodeFoo f = fooFromString . f
decodingA :: Siphon Headless ByteString (Int,Char,Bool)
decodingA = (,,)
<$> S.headless dbInt
<*> S.headless dbChar
<*> S.headless dbBool
decodingB :: Siphon Headed ByteString (Int,Word8,Bool)
decodingB = (,,)
<$> S.headed "number" dbInt
<*> S.headed "letter" dbWord8
<*> S.headed "boolean" dbBool
indexedDecodingB :: Siphon Indexed ByteString (Int,Word8,Bool)
indexedDecodingB = (,,)
<$> S.indexed 0 dbInt
<*> S.indexed 1 dbWord8
<*> S.indexed 2 dbBool
decodingG :: Siphon Headed Text ()
decodingG =
S.headed "number" (\_ -> Nothing)
<* S.headed "letter" (\_ -> Nothing)
<* S.headed "boolean" (\_ -> Nothing)
decodingF :: Siphon Headed ByteString ByteString
decodingF = S.headed "name" Just
decodingGeolite :: Siphon Headed ByteString (Int,Word8,Word8)
decodingGeolite = (,,)
<$> S.headed "network" dbInt
<*> S.headed "autonomous_system_number" dbWord8
<*> S.headed "autonomous_system_organization" dbWord8
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 = (,,)
<$> S.headless (decodeFoo Text.unpack)
<*> S.headless (decodeFoo Text.unpack)
<*> S.headless (decodeFoo Text.unpack)
encodingF :: Colonnade Headed ByteString ByteString
encodingF = headed "name" id
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
[ lmap thd3 $ headed "boolean" ebBool
, lmap snd3 $ headed "letter" ebChar
]
tripleToPairs :: (a,b,c) -> (a,(b,(c,())))
tripleToPairs (a,b,c) = (a,(b,(c,())))
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
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 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 -> 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"
propEncodeDecodeIso :: Eq a => (a -> b) -> (b -> Maybe a) -> a -> Bool
propEncodeDecodeIso f g a = g (f a) == Just a
propMatching :: Eq b => (a -> b) -> (a -> b) -> a -> Bool
propMatching f g a = f a == g a
-- | Take the first item out of a 3 element tuple
fst3 :: (a,b,c) -> a
fst3 (a,b,c) = a
-- | Take the second item out of a 3 element tuple
snd3 :: (a,b,c) -> b
snd3 (a,b,c) = b
-- | Take the third item out of a 3 element tuple
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"

490
src/Colonnade.hs Normal file
View File

@ -0,0 +1,490 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RankNTypes #-}
{- | Build backend-agnostic columnar encodings that can be
used to visualize tabular data.
-}
module Colonnade
( -- * Example
-- $setup
Colonnade
, Headed (..)
, Headless (..)
-- * Typeclasses
, E.Headedness (..)
-- * Create
, headed
, headless
, singleton
-- * Transform
-- ** Body
, fromMaybe
, columns
, bool
, replaceWhen
, modifyWhen
-- ** Header
, mapHeaderContent
, mapHeadedness
, toHeadless
-- * Cornice
-- ** Types
, Cornice
, Pillar (..)
, Fascia (..)
-- ** Create
, cap
, recap
-- * Ascii Table
, ascii
, asciiCapped
) where
import Colonnade.Encode
( Colonnade
, Cornice
, Fascia (..)
, Headed (..)
, Headless (..)
, Pillar (..)
)
import qualified Colonnade.Encode as E
import qualified Data.Bool
import Data.Foldable
import qualified Data.List as List
import qualified Data.Vector as Vector
{- $setup
First, let\'s bring in some neccessary imports that will be
used for the remainder of the examples in the docs:
>>> import Data.Monoid (mconcat,(<>))
>>> import Data.Profunctor (lmap)
The data types we wish to encode are:
>>> data Color = Red | Green | Blue deriving (Show,Eq)
>>> data Person = Person { name :: String, age :: Int }
>>> data House = House { color :: Color, price :: Int }
One potential columnar encoding of a @Person@ would be:
>>> :{
let colPerson :: Colonnade Headed Person String
colPerson = mconcat
[ headed "Name" name
, headed "Age" (show . age)
]
:}
The type signature on @colPerson@ is not neccessary
but is included for clarity. We can feed data into this encoding
to build a table:
>>> let people = [Person "David" 63, Person "Ava" 34, Person "Sonia" 12]
>>> putStr (ascii colPerson people)
+-------+-----+
| Name | Age |
+-------+-----+
| David | 63 |
| Ava | 34 |
| Sonia | 12 |
+-------+-----+
Similarly, we can build a table of houses with:
>>> let showDollar = (('$':) . show) :: Int -> String
>>> colHouse = mconcat [headed "Color" (show . color), headed "Price" (showDollar . price)]
>>> :t colHouse
colHouse :: Colonnade Headed House String
>>> let houses = [House Green 170000, House Blue 115000, House Green 150000]
>>> putStr (ascii colHouse houses)
+-------+---------+
| Color | Price |
+-------+---------+
| Green | $170000 |
| Blue | $115000 |
| Green | $150000 |
+-------+---------+
-}
-- | A single column with a header.
headed :: c -> (a -> c) -> Colonnade Headed a c
headed h = singleton (Headed h)
-- | A single column without a header.
headless :: (a -> c) -> Colonnade Headless a c
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 = 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 (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
could be represented as:
>>> :{
let owners :: [(Person,Maybe House)]
owners =
[ (Person "Jordan" 18, Nothing)
, (Person "Ruth" 25, Just (House Red 125000))
, (Person "Sonia" 12, Just (House Green 145000))
]
:}
The column encodings defined earlier can be reused with
the help of 'fromMaybe':
>>> :{
let colOwners :: Colonnade Headed (Person,Maybe House) String
colOwners = mconcat
[ lmap fst colPerson
, lmap snd (fromMaybe "" colHouse)
]
:}
>>> putStr (ascii colOwners owners)
+--------+-----+-------+---------+
| Name | Age | Color | Price |
+--------+-----+-------+---------+
| Jordan | 18 | | |
| Ruth | 25 | Red | $125000 |
| Sonia | 12 | Green | $145000 |
+--------+-----+-------+---------+
-}
fromMaybe :: c -> Colonnade f a c -> Colonnade f (Maybe a) c
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
by putting a check mark in the column corresponding to
the color instead of by writing out the name of the color:
>>> let allColors = [Red,Green,Blue]
>>> let encColor = columns (\c1 c2 -> if c1 == c2 then "" else "") (Headed . show) allColors
>>> :t encColor
encColor :: Colonnade Headed Color String
>>> let encHouse = headed "Price" (showDollar . price) <> lmap color encColor
>>> :t encHouse
encHouse :: Colonnade Headed House String
>>> putStr (ascii encHouse houses)
+---------+-----+-------+------+
| Price | Red | Green | Blue |
+---------+-----+-------+------+
| $170000 | | | |
| $115000 | | | |
| $150000 | | | |
+---------+-----+-------+------+
-}
columns ::
(Foldable g) =>
-- | Cell content function
(b -> a -> c) ->
-- | Header content function
(b -> f c) ->
-- | Basis for column encodings
g b ->
Colonnade f a c
columns getCell getHeader =
id
. E.Colonnade
. Vector.map (\b -> E.OneColonnade (getHeader b) (getCell b))
. Vector.fromList
. toList
bool ::
-- | Heading
f c ->
-- | Predicate
(a -> Bool) ->
-- | Contents when predicate is false
(a -> c) ->
-- | Contents when predicate is true
(a -> c) ->
Colonnade f a c
bool h p onTrue onFalse = singleton h (Data.Bool.bool <$> onFalse <*> onTrue <*> p)
{- | Modify the contents of cells in rows whose values satisfy the
given predicate. Header content is unaffected. With an HTML backend,
this can be used to strikethrough the contents of cells with data that is
considered invalid.
-}
modifyWhen ::
-- | Content change
(c -> c) ->
-- | Row predicate
(a -> Bool) ->
-- | Original 'Colonnade'
Colonnade f a c ->
Colonnade f a c
modifyWhen changeContent p (E.Colonnade v) =
E.Colonnade
( Vector.map
( \(E.OneColonnade h encode) -> E.OneColonnade h $ \a ->
if p a then changeContent (encode a) else encode a
)
v
)
{- | Replace the contents of cells in rows whose values satisfy the
given predicate. Header content is unaffected.
-}
replaceWhen ::
-- | New content
c ->
-- | Row predicate
(a -> Bool) ->
-- | Original 'Colonnade'
Colonnade f a c ->
Colonnade f a c
replaceWhen = modifyWhen . const
{- | Augment a 'Colonnade' with a header spans over all of the
existing headers. This is best demonstrated by example.
Let\'s consider how we might encode a pairing of the people
and houses from the initial example:
>>> let personHomePairs = zip people houses
>>> let colPersonFst = lmap fst colPerson
>>> let colHouseSnd = lmap snd colHouse
>>> putStr (ascii (colPersonFst <> colHouseSnd) personHomePairs)
+-------+-----+-------+---------+
| Name | Age | Color | Price |
+-------+-----+-------+---------+
| David | 63 | Green | $170000 |
| Ava | 34 | Blue | $115000 |
| Sonia | 12 | Green | $150000 |
+-------+-----+-------+---------+
This tabular encoding leaves something to be desired. The heading
not indicate that the name and age refer to a person and that
the color and price refer to a house. Without reaching for 'Cornice',
we can still improve this situation with 'mapHeaderContent':
>>> let colPersonFst' = mapHeaderContent ("Person " ++) colPersonFst
>>> let colHouseSnd' = mapHeaderContent ("House " ++) colHouseSnd
>>> putStr (ascii (colPersonFst' <> colHouseSnd') personHomePairs)
+-------------+------------+-------------+-------------+
| Person Name | Person Age | House Color | House Price |
+-------------+------------+-------------+-------------+
| David | 63 | Green | $170000 |
| Ava | 34 | Blue | $115000 |
| Sonia | 12 | Green | $150000 |
+-------------+------------+-------------+-------------+
This is much better, but for longer tables, the redundancy
of prefixing many column headers can become annoying. The solution
that a 'Cornice' offers is to nest headers:
>>> let cor = mconcat [cap "Person" colPersonFst, cap "House" colHouseSnd]
>>> :t cor
cor :: Cornice Headed (Cap Base) (Person, House) String
>>> putStr (asciiCapped cor personHomePairs)
+-------------+-----------------+
| Person | House |
+-------+-----+-------+---------+
| Name | Age | Color | Price |
+-------+-----+-------+---------+
| David | 63 | Green | $170000 |
| Ava | 34 | Blue | $115000 |
| Sonia | 12 | Green | $150000 |
+-------+-----+-------+---------+
-}
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:
>>> data Day = Weekday | Weekend deriving (Show)
>>> :{
let cost :: Int -> Day -> String
cost base w = case w of
Weekday -> showDollar base
Weekend -> showDollar (base + 1)
colStandard = foldMap (\c -> headed c (cost 8)) ["Yt","Ad","Sr"]
colSpecial = mconcat [headed "Stud" (cost 6), headed "Mltry" (cost 7)]
corStatus = mconcat
[ cap "Standard" colStandard
, cap "Special" colSpecial
]
corShowtime = mconcat
[ recap "" (cap "" (headed "Day" show))
, foldMap (\c -> recap c corStatus) ["Matinee","Evening"]
]
:}
>>> putStr (asciiCapped corShowtime [Weekday,Weekend])
+---------+-----------------------------+-----------------------------+
| | Matinee | Evening |
+---------+--------------+--------------+--------------+--------------+
| | Standard | Special | Standard | Special |
+---------+----+----+----+------+-------+----+----+----+------+-------+
| Day | Yt | Ad | Sr | Stud | Mltry | Yt | Ad | Sr | Stud | Mltry |
+---------+----+----+----+------+-------+----+----+----+------+-------+
| 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 h p a c -> Cornice h (Cap p) a c
recap h cor = E.CorniceCap (Vector.singleton (E.OneCornice h cor))
asciiCapped ::
(Foldable f) =>
-- | columnar encoding
Cornice Headed p a String ->
-- | rows
f a ->
String
asciiCapped cor xs =
let annCor =
E.annotateFinely
(\x y -> x + y + 3)
id
List.length
xs
cor
sizedCol = E.uncapAnnotated annCor
in E.headersMonoidal
Nothing
[
( \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
{- | Render a collection of rows as an ascii table. The table\'s columns are
specified by the given 'Colonnade'. This implementation is inefficient and
does not provide any wrapping behavior. It is provided so that users can
try out @colonnade@ in ghci and so that @doctest@ can verify example
code in the haddocks.
-}
ascii ::
(Foldable f) =>
-- | columnar encoding
Colonnade Headed a String ->
-- | rows
f a ->
String
ascii col xs =
let sizedCol = E.sizeColumns List.length xs col
divider =
concat
[ E.headerMonoidalFull
sizedCol
( \(E.Sized msz _) -> case msz of
Just sz -> "+" ++ hyphens (sz + 2)
Nothing -> ""
)
, "+\n"
]
in List.concat
[ divider
, concat
[ 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 (E.Sized (Maybe Int) Headed) a String ->
f a ->
String
asciiBody sizedCol xs =
let divider =
concat
[ E.headerMonoidalFull
sizedCol
( \(E.Sized msz _) -> case msz of
Just sz -> "+" ++ hyphens (sz + 2)
Nothing -> ""
)
, "+\n"
]
rowContents =
foldMap
( \x ->
concat
[ E.rowMonoidalHeader
sizedCol
( \(E.Sized msz _) c -> case msz of
Nothing -> ""
Just sz -> "| " ++ rightPad sz ' ' c ++ " "
)
x
, "|\n"
]
)
xs
in List.concat
[ divider
, rowContents
, divider
]
hyphens :: Int -> String
hyphens n = List.replicate n '-'
rightPad :: Int -> a -> [a] -> [a]
rightPad m a xs = take m $ xs ++ repeat a
-- data Company = Company String String Int
--
-- data Company = Company
-- { companyName :: String
-- , companyCountry :: String
-- , companyValue :: Int
-- } deriving (Show)
--
-- myCompanies :: [Company]
-- myCompanies =
-- [ Company "eCommHub" "United States" 50
-- , Company "Layer 3 Communications" "United States" 10000000
-- , Company "Microsoft" "England" 500000000
-- ]

770
src/Colonnade/Encode.hs Normal file
View File

@ -0,0 +1,770 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_HADDOCK not-home #-}
{- | 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,
a function that applies a @Colonnade Headed MyCell a@
to data will have roughly the following type:
> myTableRenderer :: Foldable g => Colonnade Headed MyCell a -> g a -> MyContent
In the companion packages @yesod-colonnade@ and
@reflex-dom-colonnade@, functions with
similar type signatures are readily available.
These packages use the functions provided here
in the implementations of their rendering functions.
It is recommended that users who believe they may need
this module look at the source of the companion packages
to see an example of how this module\'s functions are used.
Other backends are encouraged to use these functions
to build monadic or monoidal content from a 'Colonnade'.
The functions exported here take a 'Colonnade' and
convert it to a fragment of content. The functions whose
names start with @row@ take at least a @Colonnade f c a@ and an @a@
value to generate a row of content. The functions whose names
start with @header@ need the @Colonnade f c a@ but not
an @a@ value since a value is not needed to build a header.
-}
module Colonnade.Encode
( -- * Colonnade
-- ** Types
Colonnade (..)
, OneColonnade (..)
, Headed (..)
, Headless (..)
, Sized (..)
, ExtractForall (..)
-- ** Typeclasses
, Headedness (..)
-- ** Row
, row
, rowMonadic
, rowMonadic_
, rowMonadicWith
, rowMonoidal
, rowMonoidalHeader
-- ** Header
, header
, headerMonadic
, headerMonadic_
, headerMonadicGeneral
, headerMonadicGeneral_
, headerMonoidalGeneral
, headerMonoidalFull
-- ** Other
, bothMonadic_
, sizeColumns
-- * Cornice
-- ** Types
, Cornice (..)
, AnnotatedCornice (..)
, OneCornice (..)
, Pillar (..)
, ToEmptyCornice (..)
, Fascia (..)
-- ** Encoding
, annotate
, annotateFinely
, size
, endow
, discard
, headersMonoidal
, uncapAnnotated
) where
import Control.Monad.ST (ST, runST)
import Data.Foldable
import Data.Functor.Contravariant (Contravariant (..))
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Profunctor (Profunctor (..))
import Data.Vector (Vector)
import qualified Data.Semigroup as Semigroup
import qualified Data.Vector as V
import qualified Data.Vector as Vector
import qualified Data.Vector.Generic as GV
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as MVU
{- | Consider providing a variant the produces a list
instead. It may allow more things to get inlined
in to a loop.
-}
row :: (c1 -> c2) -> Colonnade f a c1 -> a -> Vector c2
row g (Colonnade v) a = flip Vector.map v $
\(OneColonnade _ encode) -> g (encode a)
bothMonadic_ ::
(Monad m) =>
Colonnade Headed a c ->
(c -> c -> m b) ->
a ->
m ()
bothMonadic_ (Colonnade v) g a =
forM_ v $ \(OneColonnade (Headed h) encode) -> g h (encode a)
rowMonadic ::
(Monad m, Monoid b) =>
Colonnade f a c ->
(c -> m b) ->
a ->
m b
rowMonadic (Colonnade v) g a =
flip foldlMapM v $
\e -> g (oneColonnadeEncode e a)
rowMonadic_ ::
(Monad m) =>
Colonnade f a c ->
(c -> m b) ->
a ->
m ()
rowMonadic_ (Colonnade v) g a =
forM_ v $ \e -> g (oneColonnadeEncode e a)
rowMonoidal ::
(Monoid m) =>
Colonnade h a c ->
(c -> m) ->
a ->
m
rowMonoidal (Colonnade v) g a =
foldMap (\(OneColonnade _ encode) -> g (encode a)) v
rowMonoidalHeader ::
(Monoid m) =>
Colonnade h a c ->
(h c -> c -> m) ->
a ->
m
rowMonoidalHeader (Colonnade v) g a =
foldMap (\(OneColonnade h encode) -> g h (encode a)) v
rowUpdateSize ::
-- | Get size from content
(c -> Int) ->
MutableSizedColonnade s h a c ->
a ->
ST s ()
rowUpdateSize toSize (MutableSizedColonnade v mv) a =
if MVU.length mv /= V.length v
then error "rowMonoidalSize: vector sizes mismatched"
else
V.imapM_
( \ix (OneColonnade _ encode) ->
MVU.modify mv (\oldSize -> max oldSize (toSize (encode a))) ix
)
v
headerUpdateSize ::
(Foldable h) =>
-- | Get size from content
(c -> Int) ->
MutableSizedColonnade s h a c ->
ST s ()
headerUpdateSize toSize (MutableSizedColonnade v mv) =
if MVU.length mv /= V.length v
then error "rowMonoidalSize: vector sizes mismatched"
else
V.imapM_
( \ix (OneColonnade h _) ->
MVU.modify mv (\oldSize -> max oldSize (foldl' (\sz c -> max sz (toSize c)) 0 h)) ix
)
v
sizeColumns ::
(Foldable f, Foldable h) =>
-- | Get size from content
(c -> Int) ->
f a ->
Colonnade h a c ->
Colonnade (Sized (Maybe Int) h) a c
sizeColumns toSize rows colonnade = runST $ do
mcol <- newMutableSizedColonnade colonnade
headerUpdateSize toSize mcol
mapM_ (rowUpdateSize toSize mcol) rows
freezeMutableSizedColonnade mcol
newMutableSizedColonnade :: Colonnade h a c -> ST s (MutableSizedColonnade s h a c)
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 (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 (Just sz) h) enc) $
V.zip v (GV.convert sizeVec)
rowMonadicWith ::
(Monad m) =>
b ->
(b -> b -> b) ->
Colonnade f a c ->
(c -> m b) ->
a ->
m b
rowMonadicWith bempty bappend (Colonnade v) g a =
foldlM
( \bl e -> do
br <- g (oneColonnadeEncode e a)
return (bappend bl br)
)
bempty
v
header :: (c1 -> c2) -> Colonnade Headed a c1 -> Vector c2
header g (Colonnade v) =
Vector.map (g . getHeaded . oneColonnadeHead) v
{- | This function is a helper for abusing 'Foldable' to optionally
render a header. Its future is uncertain.
-}
headerMonadicGeneral ::
(Monad m, Monoid b, Foldable h) =>
Colonnade h a c ->
(c -> m b) ->
m b
headerMonadicGeneral (Colonnade v) g =
id $
fmap (mconcat . Vector.toList) $
Vector.mapM (foldlMapM g . oneColonnadeHead) v
headerMonadic ::
(Monad m, Monoid b) =>
Colonnade Headed a c ->
(c -> m b) ->
m b
headerMonadic (Colonnade v) g =
fmap (mconcat . Vector.toList) $ Vector.mapM (g . getHeaded . oneColonnadeHead) v
headerMonadicGeneral_ ::
(Monad m, Headedness h) =>
Colonnade h a c ->
(c -> m b) ->
m ()
headerMonadicGeneral_ (Colonnade v) g = case headednessExtract of
Nothing -> return ()
Just f -> Vector.mapM_ (g . f . oneColonnadeHead) v
headerMonoidalGeneral ::
(Monoid m, Foldable h) =>
Colonnade h a c ->
(c -> m) ->
m
headerMonoidalGeneral (Colonnade v) g =
foldMap (foldMap g . oneColonnadeHead) v
headerMonoidalFull ::
(Monoid m) =>
Colonnade h a c ->
(h c -> m) ->
m
headerMonoidalFull (Colonnade v) g = foldMap (g . oneColonnadeHead) v
headerMonadic_ ::
(Monad m) =>
Colonnade Headed a c ->
(c -> m b) ->
m ()
headerMonadic_ (Colonnade v) g = Vector.mapM_ (g . getHeaded . oneColonnadeHead) v
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) =>
-- | fold function
(Int -> Int -> Int) ->
-- | finalize
(Int -> Int) ->
-- | Get size from content
(c -> Int) ->
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) =>
-- | Get size from content
(c -> Int) ->
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.
-- | fold function
(Int -> Int -> Int) ->
-- | finalize
(Int -> Int) ->
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) =>
-- | Apply the Fascia header row content
Maybe (Fascia p r, r -> m -> m) ->
-- | Build content from cell content and size
[(sz -> c -> m, m -> m)] ->
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}

View File

@ -1,30 +0,0 @@
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.

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,48 +0,0 @@
#!/bin/bash
set -e
if [ "$#" -ne 1 ]; then
echo "Usage: scripts/hackage-docs.sh HACKAGE_USER"
exit 1
fi
user=$1
cabal_file=$(find . -maxdepth 1 -name "*.cabal" -print -quit)
if [ ! -f "$cabal_file" ]; then
echo "Run this script in the top-level package directory"
exit 1
fi
pkg=$(awk -F ":[[:space:]]*" 'tolower($1)=="name" { print $2 }' < "$cabal_file")
ver=$(awk -F ":[[:space:]]*" 'tolower($1)=="version" { print $2 }' < "$cabal_file")
if [ -z "$pkg" ]; then
echo "Unable to determine package name"
exit 1
fi
if [ -z "$ver" ]; then
echo "Unable to determine package version"
exit 1
fi
echo "Detected package: $pkg-$ver"
dir=$(mktemp -d build-docs.XXXXXX)
trap 'rm -r "$dir"' EXIT
# cabal haddock --hoogle --hyperlink-source --html-location='/package/$pkg-$version/docs' --contents-location='/package/$pkg-$version'
stack haddock
cp -R .stack-work/dist/x86_64-linux/Cabal-1.22.5.0/doc/html/$pkg/ $dir/$pkg-$ver-docs
# /home/andrew/.stack/snapshots/x86_64-linux/lts-5.17/7.10.3/doc/index.html
tar cvz -C $dir --format=ustar -f $dir/$pkg-$ver-docs.tar.gz $pkg-$ver-docs
curl -X PUT \
-H 'Content-Type: application/x-tar' \
-H 'Content-Encoding: gzip' \
-u "$user" \
--data-binary "@$dir/$pkg-$ver-docs.tar.gz" \
"https://hackage.haskell.org/package/$pkg-$ver/docs"

View File

@ -1,183 +0,0 @@
-- | Build HTML tables using @yesod@ and @colonnade@. To learn
-- how to use this module, first read the documentation for @colonnade@,
-- and then read the documentation for @blaze-colonnade@. This library
-- and @blaze-colonnade@ are entirely distinct; neither depends on the
-- other. However, the interfaces they expose are very similar, and
-- the explanations provided counterpart are sufficient to understand
-- this library.
module Yesod.Colonnade
( -- * Build
Cell(..)
, cell
, stringCell
, textCell
, builderCell
, anchorCell
, anchorWidget
-- * Apply
, encodeWidgetTable
, encodeCellTable
, encodeDefinitionTable
, encodeListItems
) where
import Yesod.Core
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 E
import qualified Data.Text as Text
import qualified Data.Text.Lazy as LText
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 :: !(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 = (SG.<>)
-- | Create a 'Cell' from a 'Widget'
cell :: WidgetFor site () -> Cell site
cell = Cell mempty
-- | Create a 'Cell' from a 'String'
stringCell :: String -> Cell site
stringCell = cell . fromString
-- | Create a 'Cell' from a 'Text'
textCell :: Text -> Cell site
textCell = cell . toWidget . toHtml
-- | Create a 'Cell' from a text builder
builderCell :: TBuilder.Builder -> Cell site
builderCell = cell . toWidget . toHtml . LText.toStrict . TBuilder.toLazyText
-- | 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 -> WidgetFor site ()) -- ^ Content wrapped by @<a>@ tag
-> a -- ^ Value
-> Cell site
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)
-- | This determines the attributes that are added
-- to the individual @li@s by concatenating the header\'s
-- attributes with the data\'s attributes.
encodeListItems ::
(WidgetFor site () -> WidgetFor site ())
-- ^ Wrapper for items, often @ul@
-> (WidgetFor site () -> WidgetFor site () -> WidgetFor site ())
-- ^ Combines header with data
-> Colonnade Headed a (Cell site)
-- ^ How to encode data as a row
-> a
-- ^ The value to display
-> WidgetFor site ()
encodeListItems ulWrap combine enc =
ulWrap . E.bothMonadic_ enc
(\(Cell ha hc) (Cell ba bc) ->
li_ (ha <> ba) (combine hc bc)
)
-- | A two-column table with the header content displayed in the
-- first column and the data displayed in the second column. Note
-- that the generated HTML table does not have a @thead@.
encodeDefinitionTable ::
[Attribute]
-- ^ Attributes of @table@ element.
-> Colonnade Headed a (Cell site)
-- ^ How to encode data as a row
-> a
-- ^ The value to display
-> WidgetFor site ()
encodeDefinitionTable attrs enc a = table_ attrs $ tbody_ [] $
E.bothMonadic_ enc
(\theKey theValue -> tr_ [] $ do
widgetFromCell td_ theKey
widgetFromCell td_ theValue
) a
-- | 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:
--
-- > 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
-> WidgetFor site ()
encodeCellTable = encodeTable
(E.headednessPure mempty) mempty (const mempty) widgetFromCell
-- | 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
-> 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, 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
-> WidgetFor site ()
encodeTable theadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
table_ tableAttrs $ do
for_ E.headednessExtract $ \unhead ->
thead_ (unhead theadAttrs) $ do
E.headerMonadicGeneral_ colonnade (wrapContent th_)
tbody_ tbodyAttrs $ do
forM_ xs $ \x -> do
tr_ (trAttrs x) (E.rowMonadic_ colonnade (wrapContent td_) x)
widgetFromCell ::
([Attribute] -> WidgetFor site () -> WidgetFor site ())
-> Cell site
-> WidgetFor site ()
widgetFromCell f (Cell attrs contents) =
f attrs contents

View File

@ -1,33 +0,0 @@
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.9.1 && < 4.18
, 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 && < 2.1
, 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
location: https://github.com/andrewthad/colonnade