packages feed

blaze-colonnade (empty) → 0.1

raw patch · 5 files changed

+554/−0 lines, 5 filesdep +basedep +blaze-htmldep +blaze-markupsetup-changed

Dependencies added: base, blaze-html, blaze-markup, colonnade, doctest, text

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Andrew Martin (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Andrew Martin nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ blaze-colonnade.cabal view
@@ -0,0 +1,39 @@+name:                blaze-colonnade+version:             0.1+synopsis:            Helper functions for using blaze-html with colonnade+description:         Blaze HTML and colonnade+homepage:            https://github.com/andrewthad/colonnade#readme+license:             BSD3+license-file:        LICENSE+author:              Andrew Martin+maintainer:          andrew.thaddeus@gmail.com+copyright:           2017 Andrew Martin+category:            web+build-type:          Simple+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:+    Text.Blaze.Colonnade+  build-depends:+      base >= 4.7 && < 5+    , colonnade >= 1.0 && < 1.1+    , blaze-markup >= 0.7 && < 0.9+    , blaze-html >= 0.8 && < 0.10+    , text >= 1.0 && < 1.3+  default-language:    Haskell2010++test-suite test+  type:             exitcode-stdio-1.0+  hs-source-dirs:   test+  main-is:          Main.hs+  build-depends:+      base >= 4.7 && <= 5+    , colonnade+    , doctest+  default-language: Haskell2010++source-repository head+  type:     git+  location: https://github.com/andrewthad/colonnade
+ src/Text/Blaze/Colonnade.hs view
@@ -0,0 +1,477 @@+-- | 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 (encodeHeadedHtmlTable mempty col rows)+-- <table>+--     <thead><th>Grade</th><th>Letter</th></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+    encodeHeadedHtmlTable+  , encodeHeadlessHtmlTable+  , encodeHeadedCellTable+  , encodeHeadlessCellTable+  , encodeTable+    -- * Cell+    -- $build+  , Cell(..)+  , htmlCell+  , stringCell+  , textCell+  , lazyTextCell+  , builderCell+    -- * Interactive+  , printCompactHtml+  , printVeryCompactHtml+    -- * Tutorial+    -- $example++    -- * Discussion+    -- $discussion+  ) where++import Text.Blaze (Attribute,(!))+import Text.Blaze.Html (Html, toHtml)+import Colonnade (Colonnade,Headed,Headless)+import Data.Text (Text)+import Control.Monad+import Data.Monoid+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 Encode+import qualified Data.Text as Text+import qualified Data.Text.Lazy as LText+import qualified Data.Text.Lazy.Builder as TBuilder++-- $example+-- We start with a few necessary imports and some example data+-- types:+-- +-- >>> :set -XOverloadedStrings+-- >>> import Data.Monoid (mconcat,(<>))+-- >>> import Data.Char (toLower)+-- >>> import Data.Functor.Contravariant (Contravariant(contramap))+-- >>> import Colonnade (Colonnade,Headed,Headless,headed)+-- >>> import Text.Blaze.Html (Html, toHtml, toValue)+-- >>> import qualified Colonnade as C+-- >>> import qualified Text.Blaze.Html5 as H+-- >>> data Department = Management | Sales | Engineering deriving (Show,Eq)+-- >>> data Employee = Employee { name :: String, department :: Department, age :: Int }+-- +-- 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 Html Employee+--     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 (encodeHeadedHtmlTable customAttrs tableEmpA employees)+-- <table class="stylish-table" id="main-table">+--     <thead>+--         <th>Name</th>+--         <th>Age</th>+--     </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 Cell Department+--     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+-- 'encodeHeadedCellTable' instead of 'encodeHeadedHtmlTable':+--+-- >>> let twoDepts = [Sales,Management]+-- >>> printCompactHtml (encodeHeadedCellTable customAttrs tableDept twoDepts)+-- <table class="stylish-table" id="main-table">+--     <thead>+--         <th>Dept.</th>+--     </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 @Contravariant@ instance of 'Colonnade' to allow+-- this to work on @Employee@\'s instead:+--+-- >>> :t contramap+-- contramap :: Contravariant f => (a -> b) -> f b -> f a+-- >>> let tableEmpB = contramap department tableDept+-- >>> :t tableEmpB+-- tableEmpB :: Colonnade Headed Cell Employee+-- >>> printCompactHtml (encodeHeadedCellTable customAttrs tableEmpB employees)+-- <table class="stylish-table" id="main-table">+--     <thead>+--         <th>Dept.</th>+--     </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 Html Employee+-- >>> :t tableEmpB+-- tableEmpB :: Colonnade Headed Cell Employee+--+-- We can upcast the content type with 'Colonnade.mapContent'.+-- Monoidal append is then well-typed, and the resulting 'Colonnade'+-- can be applied to the employees:+--+-- >>> let tableEmpC = C.mapContent htmlCell tableEmpA <> tableEmpB+-- >>> :t tableEmpC+-- tableEmpC :: Colonnade Headed Cell Employee+-- >>> printCompactHtml (encodeHeadedCellTable customAttrs tableEmpC employees)+-- <table class="stylish-table" id="main-table">+--     <thead>+--         <th>Name</th>+--         <th>Age</th>+--         <th>Dept.</th>+--     </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 Monoid Cell where+  mempty = Cell mempty mempty+  mappend (Cell a1 c1) (Cell a2 c2) = Cell (mappend a1 a2) (mappend c1 c2)++-- | 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 ::+     (Foldable f, Foldable h)+  => Maybe Attribute -- ^ Attributes of @\<thead\>@, pass 'Nothing' to omit @\<thead\>@+  -> Attribute -- ^ Attributes of @\<tbody\>@ element+  -> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element+  -> ((Html -> Html) -> c -> Html) -- ^ Wrap content and convert to 'Html'+  -> Attribute -- ^ Attributes of @\<table\>@ element+  -> Colonnade h c a -- ^ 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+    for_ mtheadAttrs $ \theadAttrs -> do+      H.thead ! theadAttrs $ do+        Encode.headerMonoidalGeneral colonnade (wrapContent H.th)+    H.tbody ! tbodyAttrs $ do+      forM_ xs $ \x -> do+        H.tr ! trAttrs x $ Encode.rowMonoidal colonnade (wrapContent H.td) x++-- | Encode a table with a header. Table cells may have attributes+--   applied to them.+encodeHeadedCellTable :: +     Foldable f+  => Attribute -- ^ Attributes of @\<table\>@ element+  -> Colonnade Headed Cell a -- ^ How to encode data as columns+  -> f a -- ^ Collection of data+  -> Html+encodeHeadedCellTable = encodeTable+  (Just mempty) mempty (const mempty) htmlFromCell ++-- | Encode a table without a header. Table cells may have attributes+--   applied to them.+encodeHeadlessCellTable :: +     Foldable f+  => Attribute -- ^ Attributes of @\<table\>@ element+  -> Colonnade Headless Cell a -- ^ How to encode data as columns+  -> f a -- ^ Collection of data+  -> Html+encodeHeadlessCellTable = encodeTable+  Nothing mempty (const mempty) htmlFromCell ++-- | Encode a table with a header. Table cells cannot have attributes+--   applied to them.+encodeHeadedHtmlTable :: +     Foldable f+  => Attribute -- ^ Attributes of @\<table\>@ element+  -> Colonnade Headed Html a -- ^ How to encode data as columns+  -> f a -- ^ Collection of data+  -> Html+encodeHeadedHtmlTable = encodeTable+  (Just mempty) mempty (const mempty) ($) ++-- | Encode a table without a header. Table cells cannot have attributes+--   applied to them.+encodeHeadlessHtmlTable :: +     Foldable f+  => Attribute -- ^ Attributes of @\<table\>@ element+  -> Colonnade Headless Html a -- ^ How to encode data as columns+  -> f a -- ^ Collection of data+  -> Html+encodeHeadlessHtmlTable = encodeTable+  Nothing mempty (const mempty) ($) ++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" +  . removeWhitespaceAfterTag "thead"+  . 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.++
+ test/Main.hs view
@@ -0,0 +1,6 @@+import Test.DocTest++main :: IO ()+main = doctest+  [ "src/Text/Blaze/Colonnade.hs"+  ]