digestive-functors-heist 0.6.0.0 → 0.6.1.0
raw patch · 2 files changed
+618/−4 lines, 2 filesdep +blaze-builderdep ~heistdep ~mtlPVP ok
version bump matches the API change (PVP)
Dependencies added: blaze-builder
Dependency ranges changed: heist, mtl
API changes (from Hackage documentation)
+ Text.Digestive.Heist.Compiled: dfChildErrorList :: Monad m => Promise (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: dfErrorList :: Monad m => Promise (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: dfIfChildErrors :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInput :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputCheckbox :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputHidden :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputList :: Monad m => Promise (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputPassword :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputRadio :: Monad m => Promise (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputSelect :: Monad m => Promise (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputSelectGroup :: Monad m => Promise (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputSubmit :: Monad m => Splice m
+ Text.Digestive.Heist.Compiled: dfInputText :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfInputTextArea :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfLabel :: Monad m => Promise (View v) -> Splice m
+ Text.Digestive.Heist.Compiled: dfSubView :: Monad m => Promise (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: digestiveSplices :: Monad m => Promise (View Text) -> [(Text, Splice m)]
+ Text.Digestive.Heist.Compiled: formSplice :: Monad m => m (View Text) -> Splice m
+ Text.Digestive.Heist.Compiled: formSplice' :: Monad m => [(Text, Splice m)] -> [(Text, AttrSplice m)] -> m (View Text) -> Splice m
Files
digestive-functors-heist.cabal view
@@ -1,5 +1,5 @@ Name: digestive-functors-heist-Version: 0.6.0.0+Version: 0.6.1.0 Synopsis: Heist frontend for the digestive-functors library Description: Heist frontend for the digestive-functors library Homepage: http://github.com/jaspervdj/digestive-functors@@ -14,12 +14,15 @@ Library Hs-source-dirs: src GHC-options: -Wall -fwarn-tabs- Exposed-modules: Text.Digestive.Heist+ Exposed-modules:+ Text.Digestive.Heist+ Text.Digestive.Heist.Compiled Build-depends: base >= 4 && < 5,+ blaze-builder >= 0.3 && < 0.4, digestive-functors >= 0.6 && < 0.7,- heist >= 0.10.2 && < 0.11,- mtl >= 2,+ heist >= 0.11.1 && < 0.12,+ mtl >= 2 && < 2.2, text >= 0.11 && < 0.12, xmlhtml >= 0.1 && < 0.3
+ src/Text/Digestive/Heist/Compiled.hs view
@@ -0,0 +1,611 @@+--------------------------------------------------------------------------------+-- | This module provides a compiled Heist frontend for the digestive-functors+-- library.+--+-- Disclaimer: this documentation requires very basic familiarity with+-- digestive-functors. You might want to take a quick look at this tutorial+-- first:+--+-- <https://github.com/jaspervdj/digestive-functors/blob/master/examples/tutorial.lhs>+--+-- This module exports the 'formSplice' function, and most users will not+-- require anything else.+--+-- These splices are used to create HTML for different form elements. This way,+-- the developer doesn't have to care about setting e.g. the previous values in+-- a text field when something goes wrong.+--+-- For documentation on the different splices, see the different functions+-- exported by this module. All splices have the same name as given in+-- 'digestiveSplices'.+--+-- You can give arbitrary attributes to most of the elements (i.e. where it+-- makes sense). This means you can do e.g.:+--+-- > <dfInputTextArea ref="description" cols="20" rows="3" />+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+module Text.Digestive.Heist.Compiled+ ( -- * Core methods+ formSplice+ , formSplice'++ -- * Main splices+ , dfInput+ , dfInputList+ , dfInputText+ , dfInputTextArea+ , dfInputPassword+ , dfInputHidden+ , dfInputSelect+ , dfInputSelectGroup+ , dfInputRadio+ , dfInputCheckbox+ , dfInputSubmit+ , dfLabel+ , dfErrorList+ , dfChildErrorList+ , dfSubView++ -- * Utility splices+ , dfIfChildErrors+ , digestiveSplices+ ) where+++--------------------------------------------------------------------------------+import Blaze.ByteString.Builder+import Control.Monad (mplus)+import Control.Monad.Trans+import Data.Function (on)+import Data.List (unionBy)+import Data.Maybe (fromMaybe)+import Data.Monoid ((<>), mempty)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding+import Heist+import Heist.Compiled+import qualified Text.XmlHtml as X+++------------------------------------------------------------------------------+import Text.Digestive.Form.List+import Text.Digestive.View+++------------------------------------------------------------------------------+-- | List of splices defined for forms. For most uses the formSplice function+-- will be fine and you won't need to use this directly. But this is+-- available if you need more customization.+digestiveSplices :: (Monad m) => Promise (View Text) -> [(Text, Splice m)]+digestiveSplices vp =+ [ ("dfInput", dfInput vp)+ , ("dfInputText", dfInputText vp)+ , ("dfInputTextArea", dfInputTextArea vp)+ , ("dfInputPassword", dfInputPassword vp)+ , ("dfInputHidden", dfInputHidden vp)+ , ("dfInputSelect", dfInputSelect vp)+ , ("dfInputSelectGroup", dfInputSelectGroup vp)+ , ("dfInputRadio", dfInputRadio vp)+ , ("dfInputCheckbox", dfInputCheckbox vp)+ , ("dfInputFile", dfInputFile vp)+ , ("dfInputSubmit", dfInputSubmit)+ , ("dfLabel", dfLabel vp)+ , ("dfErrorList", dfErrorList vp)+ , ("dfChildErrorList", dfChildErrorList vp)+ , ("dfSubView", dfSubView vp)+ , ("dfIfChildErrors", dfIfChildErrors vp)+ , ("dfInputList", dfInputList vp)+ , ("dfEncType", dfEncType vp)+ ]+++------------------------------------------------------------------------------+-- | A compiled splice for a specific form. You pass in a runtime action that+-- gets the form's view and this function returns a splice that creates a form+-- tag. In your HeistConfig you might have a compiled splice like this:+--+-- `("customerForm", formSplice (liftM fst $ runForm "customer" custForm))`+--+-- Then you can use the customerForm tag just like you would use the dfForm+-- tag in interpreted templates anywhere you want to have a customer form.+formSplice :: Monad m => m (View Text) -> Splice m+formSplice = formSplice' [] []+++------------------------------------------------------------------------------+-- | A compiled splice for a specific form. You pass in a runtime action that+-- gets the form's view and this function returns a splice that creates a form+-- tag. In your HeistConfig you might have a compiled splice like this:+--+-- `("customerForm", formSplice (liftM fst $ runForm "customer" custForm))`+--+-- Then you can use the customerForm tag just like you would use the dfForm+-- tag in interpreted templates anywhere you want to have a customer form.+--formSplice' :: Monad m => m (View Text) -> Splice m+formSplice' :: Monad m+ => [(Text, Splice m)]+ -> [(Text, AttrSplice m)]+ -> m (View Text)+ -> Splice m+formSplice' ss as getView = do+ node <- getParamNode+ let (_, attrs) = getRefAttributes node Nothing+ tree = X.Element "form"+ (addAttrs attrs+ [ ("method", "POST")+ , ("enctype", "${dfEncType}")+ ])+ (X.childNodes node)+ action = runNode tree+ defer (\vp -> withLocalSplices (digestiveSplices vp ++ ss) as action)+ (lift getView)+++--------------------------------------------------------------------------------+attr :: Bool -> (Text, Text) -> [(Text, Text)] -> [(Text, Text)]+attr False _ = id+attr True a = (a :)+++--------------------------------------------------------------------------------+getRefAttributes :: X.Node+ -> Maybe Text -- ^ Optional default ref+ -> (Text, [(Text, Text)]) -- ^ (Ref, other attrs)+getRefAttributes node defaultRef =+ case node of+ X.Element _ as _ ->+ let ref = fromMaybe (error $ show node ++ ": missing ref") $+ lookup "ref" as `mplus` defaultRef+ in (ref, filter ((/= "ref") . fst) as)+ _ -> (error "Wrong type of node!", [])+++dfEncType :: (Monad m)+ => Promise (View v)+ -> Splice m+dfEncType p = do+ return $ yieldRuntime $ do+ view <- getPromise p+ return $ fromByteString $ encodeUtf8 $ T.pack (show $ viewEncType view)+++dfMaster :: Monad m+ => (Text -> [(Text, Text)] -> View v -> RuntimeSplice m Builder)+ -> Promise (View v) -> Splice m+dfMaster f p = do+ node <- getParamNode+ let (ref, attrs) = getRefAttributes node Nothing+ runAttrs <- runAttributesRaw attrs+ return $ yieldRuntime $ do+ view <- getPromise p+ attrs' <- runAttrs+ f ref attrs' view+++dfTag :: (Monad m)+ => (Text -> [(Text, Text)] -> Text -> [X.Node])+ -> Promise (View v)+ -> Splice m+dfTag f = dfMaster $ \ref attrs view -> do+ let ref' = absoluteRef ref view+ -- If there is no bang pattern on value, then for some reason it+ -- doesn't get forced and errors aren't displayed properly.+ !value = fieldInputText ref view+ return $ X.renderHtmlFragment X.UTF8 $ f ref' attrs value+++dfInputGeneric :: Monad m+ => [(Text, Text)]+ -> Promise (View v)+ -> Splice m+dfInputGeneric as = dfTag $ \ref attrs value ->+ makeElement "input" [] $ addAttrs attrs $+ as ++ [("id", ref), ("name", ref), ("value", value)]+++--------------------------------------------------------------------------------+-- | Generate a submit button. Example:+--+-- > <dfInputSubmit />+dfInputSubmit :: Monad m => Splice m+dfInputSubmit = do+ node <- getParamNode+ let (_, attrs) = getRefAttributes node Nothing+ runAttrs <- runAttributesRaw attrs+ return $ yieldRuntime $ do+ attrs' <- runAttrs+ let e = makeElement "input" [] $ addAttrs attrs'+ [("type", "submit")]+ return $ X.renderHtmlFragment X.UTF8 e+++--------------------------------------------------------------------------------+-- | Generate a label for a field. Example:+--+-- > <dfLabel ref="user.married">Married: </dfLabel>+-- > <dfInputCheckbox ref="user.married" />+dfLabel :: Monad m => Promise (View v) -> Splice m+dfLabel p = do+ node <- getParamNode+ let (ref, attrs) = getRefAttributes node Nothing+ runAttrs <- runAttributesRaw attrs+ return $ yieldRuntime $ do+ view <- getPromise p+ attrs' <- runAttrs+ let ref' = absoluteRef ref view+ e = makeElement "label" (X.childNodes node) $ addAttrs attrs'+ [("for", ref')]+ return $ X.renderHtmlFragment X.UTF8 e+++--------------------------------------------------------------------------------+-- | Generate an input field with a supplied type. Example:+--+-- > <dfInput type="date" ref="date" />+dfInput :: Monad m => Promise (View v) -> Splice m+dfInput = dfInputGeneric []+++--------------------------------------------------------------------------------+-- | Generate a text input field. Example:+--+-- > <dfInputText ref="user.name" />+dfInputText :: Monad m => Promise (View v) -> Splice m+dfInputText = dfInputGeneric [("type", "text")]+++--------------------------------------------------------------------------------+-- | Generate a text area. Example:+--+-- > <dfInputTextArea ref="user.about" />+dfInputTextArea :: Monad m => Promise (View v) -> Splice m+dfInputTextArea = dfTag $ \ref attrs value ->+ makeElement "textarea" [X.TextNode value] $ addAttrs attrs+ [("id", ref), ("name", ref)]+++--------------------------------------------------------------------------------+-- | Generate a password field. Example:+--+-- > <dfInputPassword ref="user.password" />+dfInputPassword :: Monad m => Promise (View v) -> Splice m+dfInputPassword = dfInputGeneric [("type", "password")]+++--------------------------------------------------------------------------------+-- | Generate a hidden input field. Example:+--+-- > <dfInputHidden ref="user.forgery" />+dfInputHidden :: Monad m => Promise (View v) -> Splice m+dfInputHidden = dfInputGeneric [("type", "hidden")]+++--------------------------------------------------------------------------------+-- | Generate a checkbox. Example:+--+-- > <dfInputCheckbox ref="user.married" />+dfInputCheckbox :: Monad m+ => Promise (View v)+ -> Splice m+dfInputCheckbox = dfMaster $ \ref attrs view -> do+ let ref' = absoluteRef ref view+ value = fieldInputBool ref view+ e = makeElement "input" [] $ addAttrs attrs $+ attr value ("checked", "checked") $+ [("type", "checkbox"), ("id", ref'), ("name", ref')]++ return $ X.renderHtmlFragment X.UTF8 e+++--------------------------------------------------------------------------------+-- | Generate a file upload element. Example:+--+-- > <dfInputFile ref="user.avatar" />+dfInputFile :: Monad m => Promise (View v) -> Splice m+dfInputFile = dfMaster $ \ref attrs view -> do+ let ref' = absoluteRef ref view+ value = maybe "" T.pack $ fieldInputFile ref view+ e = makeElement "input" [] $ addAttrs attrs $+ [ ("type", "file"), ("id", ref')+ , ("name", ref'), ("value", value)]+ return $ X.renderHtmlFragment X.UTF8 e+++--------------------------------------------------------------------------------+-- | Generate a select button (also known as a combo box). Example:+--+-- > <dfInputSelect ref="user.sex" />+dfInputSelect :: Monad m => Promise (View Text) -> Splice m+dfInputSelect = dfMaster $ \ref attrs view -> do+ let ref' = absoluteRef ref view+ choices = fieldInputChoice ref view+ kids = map makeOption choices+ value i = ref' <> "." <> i++ makeOption (i, c, sel) = X.Element "option"+ (attr sel ("selected", "selected") [("value", value i)])+ [X.TextNode c]++ e = makeElement "select" kids $ addAttrs attrs+ [("id", ref'), ("name", ref')]+ return $ X.renderHtmlFragment X.UTF8 e+++--------------------------------------------------------------------------------+-- | Generate a select button (also known as a combo box). Example:+--+-- > <dfInputSelectGroup ref="user.sex" />+dfInputSelectGroup :: Monad m => Promise (View Text) -> Splice m+dfInputSelectGroup = dfMaster $ \ref attrs view -> do+ let ref' = absoluteRef ref view+ choices = fieldInputChoiceGroup ref view+ kids = map makeGroup choices+ value i = ref' <> "." <> i+ + makeGroup (name, options) = X.Element "optgroup"+ [("label", name)] $ map makeOption options+ makeOption (i, c, sel) = X.Element "option"+ (attr sel ("selected", "selected") [("value", value i)])+ [X.TextNode c]+ + e = makeElement "select" kids $ addAttrs attrs+ [("id", ref'), ("name", ref')]+ return $ X.renderHtmlFragment X.UTF8 e+++--------------------------------------------------------------------------------+-- | Generate a number of radio buttons. Example:+--+-- > <dfInputRadio ref="user.sex" />+dfInputRadio :: Monad m => Promise (View Text) -> Splice m+dfInputRadio = dfMaster $ \ref attrs view -> do+ let ref' = absoluteRef ref view+ choices = fieldInputChoice ref view+ kids = concatMap makeOption choices+ value i = ref' <> "." <> i+ + makeOption (i, c, sel) =+ [ X.Element "input"+ (attr sel ("checked", "checked") $ addAttrs attrs+ [ ("type", "radio"), ("value", value i)+ , ("id", value i), ("name", ref')+ ]) []+ , X.Element "label" [("for", value i)] [X.TextNode c]+ ]+ + return $ X.renderHtmlFragment X.UTF8 kids+++--------------------------------------------------------------------------------+-- | Display the list of errors for a certain field. Example:+--+-- > <dfErrorList ref="user.name" />+-- > <dfInputText ref="user.name" />+dfErrorList :: Monad m => Promise (View Text) -> Splice m+dfErrorList p = do+ node <- getParamNode+ return $ yieldRuntime $ do+ view <- getPromise p+ let (ref, attrs) = getRefAttributes node Nothing+ nodes = errorList (errors ref view) attrs+ return $ X.renderHtmlFragment X.UTF8 nodes+++--------------------------------------------------------------------------------+-- | Display the list of errors for a certain form and all forms below it. E.g.,+-- if there is a subform called @\"user\"@:+--+-- > <dfChildErrorList ref="user" />+--+-- Or display /all/ errors for the form:+--+-- > <dfChildErrorList ref="" />+--+-- Which is more conveniently written as:+--+-- > <dfChildErrorList />+dfChildErrorList :: Monad m => Promise (View Text) -> Splice m+dfChildErrorList p = do+ node <- getParamNode+ return $ yieldRuntime $ do+ view <- getPromise p+ let (ref, attrs) = getRefAttributes node (Just "")+ nodes = errorList (childErrors ref view) attrs+ return $ X.renderHtmlFragment X.UTF8 nodes+++--------------------------------------------------------------------------------+-- | Render some content only if there are any errors. This is useful for markup+-- purposes.+--+-- > <dfIfChildErrors ref="user">+-- > Content to be rendered if there are any errors...+-- > </dfIfChildErrors>+--+-- The @ref@ attribute can be omitted if you want to check the entire form.+dfIfChildErrors :: (Monad m) => Promise (View v) -> Splice m+dfIfChildErrors p = do+ node <- getParamNode+ return $ yieldRuntime $ do+ view <- getPromise p+ let (ref, _) = getRefAttributes node Nothing+ if null (childErrors ref view)+ then return mempty+ else return $ X.renderHtmlFragment X.UTF8 (X.childNodes node)+++--------------------------------------------------------------------------------+-- | This splice allows reuse of templates by selecting some child of a form+-- tree. While this may sound complicated, it's pretty straightforward and+-- practical. Suppose we have:+--+-- > <dfInputText ref="user.name" />+-- > <dfInputText ref="user.password" />+-- >+-- > <dfInputTextArea ref="comment.body" />+--+-- You may want to abstract the @\"user\"@ parts in some other template so you+-- Don't Repeat Yourself (TM). If you create a template called @\"user-form\"@+-- with the following contents:+--+-- > <dfInputText ref="name" />+-- > <dfInputText ref="password" />+--+-- You will be able to use:+--+-- > <dfSubView ref="user">+-- > <apply template="user-form" />+-- > </dfSubView>+-- >+-- > <dfInputTextArea ref="comment.body" />+dfSubView :: Monad m => Promise (View Text) -> Splice m+dfSubView p = do+ node <- getParamNode+ p2 <- newEmptyPromise+ let action = yieldRuntimeEffect $ do+ view <- getPromise p+ let (ref, _) = getRefAttributes node Nothing+ view' = subView ref view+ putPromise p2 view'+ res <- withLocalSplices (digestiveSplices p2) [] $+ runNodeList $ X.childNodes node+ return $ action <> res+++dfSingleListItem :: Monad m+ => X.Node+ -> (Promise (View Text) -> AttrSplice m)+ -> Promise (View Text)+ -> HeistT m IO (RuntimeSplice m Builder)+dfSingleListItem node attrs viewPromise = do+ p2 <- newEmptyPromise+ let action = yieldRuntimeEffect $ do+ view <- getPromise viewPromise+ putPromise p2 view+ res <- withLocalSplices (digestiveSplices p2)+ [("itemAttrs", attrs viewPromise)]+ (runNodeList $ X.childNodes node)+ return $ codeGen $ action <> res+++--------------------------------------------------------------------------------+-- | This splice allows variable length lists. It binds several attribute+-- splices providing functionality for dynamically manipulating the list. The+-- following descriptions will use the example of a form named \"foo\" with a+-- list subform named \"items\".+--+-- Splices:+-- dfListItem - This tag must surround the markup for a single list item.+-- It surrounds all of its children with a div with id \"foo.items\" and+-- class \"inputList\".+-- +-- Attribute Splices:+-- itemAttrs - Attribute you should use on div, span, etc that surrounds all+-- the markup for a single list item. This splice expands to an id of+-- \"foo.items.ix\" (where ix is the index of the current item) and a+-- class of \"inputListItem\".+-- addControl - Use this attribute on the tag you use to define a control+-- for adding elements to the list (usually a button or anchor). It adds+-- an onclick attribute that calls a javascript function addInputListItem.+-- removeControl - Use this attribute on the control for removing individual+-- items. It adds an onclick attribute that calls removeInputListItem.+dfInputList :: Monad m => Promise (View Text) -> Splice m+dfInputList p = do+ node <- getParamNode+ itemsPromise <- newEmptyPromise+ refPromise <- newEmptyPromise+ indicesPromise <- newEmptyPromise+ templateViewPromise <- newEmptyPromise+ let itemAttrs viewPromise _ = do+ view <- getPromise viewPromise+ listRef <- getPromise refPromise+ return+ [ ("id", T.concat [listRef, ".", last $ "0" : viewContext view])+ , ("class", T.append listRef ".inputListItem")+ ]+ templateAttrs viewPromise _ = do+ view <- getPromise viewPromise+ listRef <- getPromise refPromise+ return+ [ ("id", T.concat [listRef, ".", last $ "-1" : viewContext view])+ , ("class", T.append listRef ".inputListTemplate")+ , ("style", "display: none;")+ ]+ dfListItem = do+ n <- getParamNode+ template <- dfSingleListItem n templateAttrs+ templateViewPromise+ body <- mapPromises (dfSingleListItem n itemAttrs) $+ getPromise itemsPromise+ return $ yieldRuntime template <> body+ let listAttrs =+ [ ("id", "${dfListRef}")+ , ("class", "inputList")+ ]+ indices = X.Element "input"+ [ ("type", "hidden")+ , ("name", T.intercalate "." ["${dfListRef}", indicesRef])+ , ("value", "${dfIndicesList}")+ ] []+ e = X.Element "div" listAttrs (indices : X.childNodes node)+ let addControl _ = do+ listRef <- getPromise refPromise+ return [ ("onclick", T.concat [ "addInputListItem(this, '"+ , listRef+ , "'); return false;"] ) ]+ removeControl _ = do+ listRef <- getPromise refPromise+ return [ ("onclick", T.concat [ "removeInputListItem(this, '"+ , listRef+ , "'); return false;"] ) ]+ attrSplices = [ ("addControl", addControl)+ , ("removeControl", removeControl)+ ]+ splices = [ ("dfListRef", return $ yieldRuntimeText $ getPromise refPromise)+ , ("dfIndicesList", return $ yieldRuntimeText $ getPromise indicesPromise)+ , ("dfListItem", dfListItem)+ ]++ -- The runtime action that gets the right data and puts it in promises.+ let action = yieldRuntimeEffect $ do+ view <- getPromise p+ let (ref, _) = getRefAttributes node Nothing+ listRef = absoluteRef ref view+ items = listSubViews ref view+ tview = makeListSubView ref (-1) view+ putPromise refPromise listRef+ putPromise indicesPromise $ T.intercalate "," $+ map (last . ("0":) . viewContext) items+ putPromise itemsPromise items+ putPromise templateViewPromise tview++ -- At the core, this function just runs its children with a few splices+ -- and attribute splices bound. + res <- withLocalSplices splices attrSplices $ runNode e+ return $ action <> res+++--------------------------------------------------------------------------------+makeElement :: Text -> [X.Node] -> [(Text, Text)] -> [X.Node]+makeElement name nodes = return . flip (X.Element name) nodes+++--------------------------------------------------------------------------------+-- | Does not override existing attributes+addAttrs :: [(Text, Text)] -- ^ Original attributes+ -> [(Text, Text)] -- ^ Attributes to add+ -> [(Text, Text)] -- ^ Resulting attributes+addAttrs = unionBy (on (==) fst)+++--------------------------------------------------------------------------------+errorList :: [Text] -> [(Text, Text)] -> [X.Node]+errorList [] _ = []+errorList errs attrs = [X.Element "ul" attrs $ map makeError errs]+ where+ makeError e = X.Element "li" [] [X.TextNode e]++