packages feed

pandoc-utils (empty) → 0.4.0

raw patch · 9 files changed

+839/−0 lines, 9 filesdep +basedep +containersdep +data-defaultsetup-changed

Dependencies added: base, containers, data-default, pandoc, pandoc-types, pandoc-utils, tasty, tasty-hspec, text, transformers

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Releases++## pandoc-utils 0.4.0 (2020-5-24)++- Initial public release.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2020 Krasjet++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,103 @@+# pandoc-utils++This package contains some useful functions for writing+[Pandoc](https://pandoc.org/) filters and integrating Pandoc into Haskell+applications such as [Hakyll](https://jaspervdj.be/hakyll/). It provides a+composable wrapper for filters acting on nodes of the [Pandoc+AST](https://hackage.haskell.org/package/pandoc-types/docs/Text-Pandoc-Definition.html).++## Filter conversion/composition++As an example, let us look at the `behead` and `delink` filter from [Pandoc's+tutorial](https://pandoc.org/filters.html).+```haskell+behead :: Block -> Block+behead (Header n _ xs) | n >= 2 = Para [Emph xs]+behead x = x++delink :: Inline -> [Inline]+delink (Link _ txt _) = txt+delink x = [x]+```++Since `behead` has type `Block -> Block`, while `delink` has type `Inline ->+[Inline]`, they are not naturally composable. However, this package provides a+utility function `mkFilter` to convert them into a `PandocFilter`.+```haskell+import Text.Pandoc.Filter.Utils++beheadFilter :: PandocFilter+beheadFilter = mkFilter behead++delinkFilter :: PandocFilter+delinkFilter = mkFilter delink+```+`PandocFilter` is an alias for `PartialFilter Pandoc`, so you can also have+`PartialFilter Inline`, etc. There is also a monadic version called+`PartialFilterM` for encapsulating monadic filters.++The `PandocFilter` is a monoid so you can do something like,+```haskell+myFilter :: PandocFilter+myFilter = beheadFilter <> delinkFilter+```+where `myFilter` would apply `beheadFilter` first, then the `delinkFilter`. You+can apply the filter using `applyFilter`,+```haskell+import Text.Pandoc+import Data.Default (def)++mdToHtml+  :: Text                    -- ^ Input markdown string+  -> Either PandocError Text -- ^ Html string or error+mdToHtml md = runPure $ do+  doc <- readMarkdown def md+  let doc' = applyFilter myFilter doc+  writeHtml5String def doc'+```+or get an unwrapped `Pandoc -> Pandoc` filter using `getFilter` (this function+is also capable of doing implicit conversion from `PartialFilter a` to `b ->+b`).+```haskell+myPandocFilter :: Pandoc -> Pandoc+myPandocFilter = getFilter myFilter+```++For applying multiple filters, there is also a function called `applyFilters`,+which takes a list of filters and apply it to a `Pandoc` document (or subnode)+sequentially, from left to right.+```haskell+myFilters :: [PandocFilter]+myFilters =+  [ beheadFilter+  , delinkFilter+  ]++mdToHtml'+  :: Text                    -- ^ Input markdown string+  -> Either PandocError Text -- ^ Html string or error+mdToHtml' md = runPure $ do+  doc <- readMarkdown def md+  let doc' = applyFilters myFilters doc+  writeHtml5String def doc'+```++## Attribute builder++pandoc-utils also provides an attribute builder for handling attributes. You+can create a new attributes by+```haskell+ghci> import Text.Pandoc.Filter.Utils.AttrBuilder+ghci> import Text.Pandoc.Definition+ghci> nullAttr `setId` "id" `addClass` "class" `addKVPair` ("key","value")+("id",["class"],[("key","value")])+```+or modifying an existing attributes by+```haskell+ghci> attr = ("id",[],[])+ghci> attr `setId` "newId"+("newId",[],[])+```++For more examples, please read the+[spec](https://github.com/Krasjet/pandoc-utils/blob/master/test/Spec.hs).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ pandoc-utils.cabal view
@@ -0,0 +1,61 @@+build-type:          Simple+cabal-version:       2.0++name:                pandoc-utils+version:             0.4.0+synopsis:+  Utility functions to work with Pandoc in Haskell applications.+description:+  This package contains some useful functions for writing [Pandoc](https://pandoc.org/)+  filters and integrating Pandoc into Haskell applications such+  as [Hakyll](https://jaspervdj.be/hakyll/). It provides a+  composable wrapper for filters acting on nodes of the [Pandoc+  AST](https://hackage.haskell.org/package/pandoc-types/docs/Text-Pandoc-Definition.html).+  .+  For more examples, please see the [README](https://github.com/Krasjet/pandoc-utils) on GitHub.+license:             MIT+license-file:        LICENSE+author:              Krasjet+maintainer:          Krasjet+copyright:           Copyright (c) 2020 Krasjet+homepage:            https://github.com/Krasjet/pandoc-utils+bug-reports:         https://github.com/Krasjet/pandoc-utils/issues+category:            Text+extra-source-files:+    CHANGELOG.md+    README.md++source-repository head+  type:     git+  location: git://github.com/Krasjet/pandoc-utils.git++library+  hs-source-dirs:      src+  default-language:    Haskell2010+  ghc-options:         -Wall+  exposed-modules:+      Text.Pandoc.Utils+      Text.Pandoc.Filter.Utils+      Text.Pandoc.Filter.Utils.AttrBuilder+  build-depends:+      base          >=4.10 && <4.15+    , pandoc-types ^>=1.17.2 || ^>=1.20+    , text         ^>=1.2++test-suite pandoc-utils-test+  hs-source-dirs:      test+  default-language:    Haskell2010+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -Wall+  type:                exitcode-stdio-1.0+  main-is:             Spec.hs+  build-depends:+      base          >=4.10 && <4.15+    , pandoc-utils+    , containers   ^>=0.6+    , data-default ^>=0.7+    , pandoc       ^>=2.9+    , pandoc-types ^>=1.20+    , transformers ^>=0.5+    , tasty        ^>=1.2+    , tasty-hspec  ^>=1.1+    , text         ^>=1.2
+ src/Text/Pandoc/Filter/Utils.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- | This module contains some utility functions to work with different levels+-- of Pandoc filters. For example, for the conversion from @'Inline' ->+-- ['Inline']@ to @'Pandoc' -> 'Pandoc'@ filter.+module Text.Pandoc.Filter.Utils (+  -- * Definitions+  PartialFilterM,+  PartialFilter,+  PandocFilterM,+  PandocFilter,+  -- * Filter application+  applyFilterM,+  applyFilter,+  -- * Filter composition+  applyFiltersM,+  applyFilters,+  -- * Filter conversion+  getFilterM,+  getFilter,+  getConcatedFilterM,+  getConcatedFilter,+  ToPartialFilter (..),+  mkConcatedFilter,+  toFilterM,+  ) where++import Control.Monad          ((>=>))+import Data.Foldable          (fold)+import Data.Functor.Identity  (Identity (..))+import Text.Pandoc.Definition+import Text.Pandoc.Walk++-- | @PartialFilterM m p@ is a wrapper for any monadic @p -> m p@ Pandoc+-- filters acting on a subnode (e.g. 'Inline' or 'Block') of the 'Pandoc'+-- abstract syntax tree.+--+-- * @m@: a monad.+-- * @p@: the type of a subnode of 'Pandoc' (e.g. 'Inline').+newtype PartialFilterM m p =+  PartialFilterM+    { -- | Apply a monadic filter to @p@.+      applyFilterM :: p -> m p+    }++-- | @PartialFilter p@ is a wrapper for ordinary @p -> p@ Pandoc filters acting+-- on a subnode (e.g. 'Inline' or 'Block') of the 'Pandoc' abstract syntax+-- tree.+--+-- * @p@: the type of a subnode of 'Pandoc' (e.g. 'Inline').+type PartialFilter = PartialFilterM Identity++-- | An alias for @PartialFilter Pandoc@. It encapsulates a monadic+-- filter @'Pandoc' -> m 'Pandoc'@ acting directly on 'Pandoc'.+--+-- * @m@: a monad.+type PandocFilter = PartialFilter Pandoc++-- | An alias for @PartialFilterM m Pandoc@, a monadic version of+-- 'PandocFilter'.+--+-- * @m@: a monad.+type PandocFilterM m = PartialFilterM m Pandoc++-- | Apply an ordinary filter to @p@, which returns @p@ directly.+applyFilter+  :: PartialFilter p -- ^ A wrapped partial filter.+  -> p               -- ^ 'Pandoc' AST node.+  -> p               -- ^ Transformed node.+applyFilter = (runIdentity .) . applyFilterM++-- | It is mostly the same as 'applyFilterM' and should be used when you don't+-- need to apply the filter immediately. The only difference is that it will+-- perform an implicit conversion if the requested filter function is of a+-- different type.+getFilterM+  :: (Monad m, Walkable a b)+  => PartialFilterM m a -- ^ A wrapped partial filter on @a@.+  -> (b -> m b)         -- ^ Unwrapped filter function that can be directly applied to @b@.+getFilterM = applyFilterM . mkFilter++-- | It is mostly the same as 'applyFilter' and should be used when you don't+-- need to apply the filter immediately. The only difference is that it will+-- perform an implicit conversion if the requested filter function is of a+-- different type.+getFilter+  :: (Walkable a b)+  => PartialFilter a -- ^ A wrapped partial filter on @a@.+  -> (b -> b)        -- ^ Unwrapped filter function that can be directly applied to @b@.+getFilter = applyFilter . mkFilter++-- | The 'Semigroup' instance of `PartialFilterM`. @f1 <> f2@ will apply @f1@+-- first followed by @f2@.+instance (Monad m) => Semigroup (PartialFilterM m p) where+  f1 <> f2 = PartialFilterM (applyFilterM f1 >=> applyFilterM f2)++-- | The 'Monoid' instance of `PartialFilterM`.+instance (Monad m) => Monoid (PartialFilterM m p) where+  mempty = PartialFilterM return++-- | A helper typeclass used as a polymorphic constructor of 'PartialFilterM'.+class ToPartialFilter m f p where+  -- | The actual constructor of 'PartialFilterM'. It takes an ordinary filter+  -- function @a -> b@ and wraps it as a 'PartialFilterM'. It can also be used+  -- to convert between different types of @'PartialFilterM' m@.+  mkFilter+    :: f                  -- ^ A partial filter function, usually @a -> a@ for some @'Walkable' a p@.+    -> PartialFilterM m p -- ^ Wrapped partial Pandoc filter.++instance (Monad m, Walkable a p) => ToPartialFilter m (a -> a) p where+  mkFilter = PartialFilterM . (return .) . walk++instance (Monad m, Walkable a p) => ToPartialFilter m (a -> m a) p where+  mkFilter = PartialFilterM . walkM++instance (Monad m, Walkable [a] p) => ToPartialFilter m (a -> [a]) p where+  mkFilter = PartialFilterM . (return .) . walk . concatMap++instance (Monad m, Walkable [a] p) => ToPartialFilter m (a -> m [a]) p where+  mkFilter = PartialFilterM . walkM . (fmap concat .) . mapM++-- | This instance can be used to convert @'PartialFilterM' m a@ to+-- @'PartialFilterM' m b@.+instance (Monad m, Walkable a b) => ToPartialFilter m (PartialFilterM m a) b where+  mkFilter = mkFilter . applyFilterM++-- | Construct a 'PartialFilterM' from a list of filter functions of the same+-- type. The final filter is concatenated from left to right such that the+-- first element in the list will be applied first and the last element will be+-- applied at the end.+mkConcatedFilter+  :: (Monad m, ToPartialFilter m f p, Foldable t)+  => t f                -- ^ A list of filter functions of the same type.+  -> PartialFilterM m p -- ^ Concatenated filter.+mkConcatedFilter = foldMap mkFilter++-- | Convert an ordinary 'PartialFilter' to the monadic version+-- 'PartialFilterM'.+toFilterM+  :: (Monad m)+  => PartialFilter p    -- ^ An ordinary filter.+  -> PartialFilterM m p -- ^ The monadic version.+toFilterM = PartialFilterM . (return .) . applyFilter++-- | Apply a list of monadic partial filters sequentially, from left to+-- right, i.e.  the first element in the list will be applied first and the+-- last element will be applied at the end.+applyFiltersM+  :: (Foldable t, Monad m)+  => t (PartialFilterM m p) -- ^ A list of monadic partial filters.+  -> p                      -- ^ 'Pandoc' AST node.+  -> m p                    -- ^ Transformed node.+applyFiltersM = applyFilterM . fold++-- | Apply a list of partial filters sequentially, from left to right, i.e.+-- the first element in the list will be applied first and the last element+-- will be applied at the end.+applyFilters+  :: (Foldable t)+  => t (PartialFilter p) -- ^ A list of partial filter.+  -> p                   -- ^ 'Pandoc' AST node.+  -> p                   -- ^ Transformed node.+applyFilters = applyFilter . fold++-- | An alias for 'applyFiltersM', used when the filter is not used+-- immediately.+getConcatedFilterM+  :: (Foldable t, Monad m)+  => t (PartialFilterM m p) -- ^ A list of monadic partial filters.+  -> (p -> m p)             -- ^ Unwrapped monadic filter applicable to @p@ directly.+getConcatedFilterM = applyFiltersM++-- | An alias for 'applyFilters', used when the filter is not used immediately.+getConcatedFilter+  :: (Foldable t)+  => t (PartialFilter p) -- ^ A list of partial filter.+  -> (p -> p)            -- ^ Unwrapped filter applicable to @p@ directly.+getConcatedFilter = applyFilters
+ src/Text/Pandoc/Filter/Utils/AttrBuilder.hs view
@@ -0,0 +1,52 @@+-- | This module contains some utility functions for building+-- attributes.+module Text.Pandoc.Filter.Utils.AttrBuilder (+  -- * Attribute builders+  addClass,+  addClasses,+  addKVPair,+  addKVPairs,+  setId,+) where++import Text.Pandoc.Utils++import Data.Bifunctor         (bimap, first, second)+import Data.Text              (Text)+import Text.Pandoc.Definition++-- | Append a new class to attributes.+addClass+  :: Attr -- ^ Original attributes.+  -> Text -- ^ Name of the class.+  -> Attr -- ^ New attributes.+attr `addClass` cls = first (fromText cls:) attr+  -- (i, fromText newCls:cls, p)++-- | Append a list of classes to attributes.+addClasses+  :: Attr   -- ^ Original attributes.+  -> [Text] -- ^ A list of class names.+  -> Attr   -- ^ New attributes.+attr `addClasses` clses = first (map fromText clses<>) attr++-- | Append a new key-value pair to attributes.+addKVPair+  :: Attr         -- ^ Original attributes.+  -> (Text, Text) -- ^ A key-value pair.+  -> Attr         -- ^ New attributes.+attr `addKVPair` p = second (bimap fromText fromText p:) attr++-- | Append new key-value pairs to attributes.+addKVPairs+  :: Attr           -- ^ Original attributes.+  -> [(Text, Text)] -- ^ A list of key-value pairs.+  -> Attr           -- ^ New attributes.+attr `addKVPairs` ps = second (map (bimap fromText fromText) ps <>) attr++-- | Set the id of attributes.+setId+  :: Attr -- ^ Original attributes.+  -> Text -- ^ The id of the attrbute.+  -> Attr -- ^ New attributes.+(_, cls, p) `setId` i = (i, cls, p)
+ src/Text/Pandoc/Utils.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE TypeFamilies #-}++-- | Due to the switch from 'String' to 'Text' in pandoc-types 1.20, some+-- legacy filters might not work in newer versions. This module contains some+-- string conversion utility functions to make filters work in across different+-- versions.+module Text.Pandoc.Utils (+  -- * Conversions+  ToString (..),+  ToText (..),+  IsText (..),+  -- * Reexport from Data.String+  IsString (..),+  ) where++import qualified Data.Text as T++import Data.String (IsString (..))+import Data.Text   (Text)++-- | A helper typeclass for converting 'String' and 'Text' to 'String'.+class IsString s => ToString s where+  -- | Convert strings to 'String'.+  toString :: s -> String++instance Char ~ c => ToString [c] where+  toString = id++instance ToString Text where+  toString = T.unpack++-- | A helper typeclass for converting 'String' and 'Text' to 'Text'.+class IsString s => ToText s where+  -- | Convert strings to 'Text'.+  toText :: s -> Text++instance Char ~ c => ToText [c] where+  toText = T.pack++instance ToText Text where+  toText = id++-- | A helper typeclass for converting 'Text' to either 'String' or 'Text'.+class IsText a where+  -- | Convert from 'Text' to strings+  fromText :: Text -> a++instance Char ~ c => IsText [c] where+  fromText = T.unpack++instance IsText Text where+  fromText = id
+ test/Spec.hs view
@@ -0,0 +1,364 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import qualified Data.Text   as T+import qualified Text.Pandoc as P++import Control.Monad.Trans.Writer+import Data.Default                         (def)+import Data.Either                          (fromRight)+import Data.Text                            (Text)+import Test.Tasty+import Test.Tasty.Hspec+import Text.Pandoc.Definition+import Text.Pandoc.Filter.Utils+import Text.Pandoc.Filter.Utils.AttrBuilder++-- * Testing data++-- ** Partial level++blockPara :: Block+blockPara = Para [Str "abcd"]++expectedInlinePartial :: Block+expectedInlinePartial = Para [Str "ABCD"]++expectedInlinePartialL :: Block+expectedInlinePartialL = Para [Str "abcd", Str "abcd"]++-- ** Pandoc level++docPara :: Pandoc+docPara = Pandoc (Meta mempty) [blockPara]++expectedInline :: Pandoc+expectedInline = Pandoc (Meta mempty) [Para [Str "ABCD"]]++expectedBlock :: Pandoc+expectedBlock = Pandoc (Meta mempty) [Plain [Str "abcd"]]++expectedInlineL :: Pandoc+expectedInlineL = Pandoc (Meta mempty) [Para [Str "abcd", Str "abcd"]]++expectedBlockL :: Pandoc+expectedBlockL = Pandoc (Meta mempty) [blockPara, blockPara]++-- ** Composition results++compPara :: Block+compPara = Para [Str "abcd"]++compPara2 :: Block+compPara2 = Para [Str "abcd", Str "efgh"]++expectedDup :: Block+expectedDup = Para [Str "abcd", Str "abcd"]++expectedMerge :: Block+expectedMerge = Para [Str "abcdefgh"]++expectedDupMerge :: Block+expectedDupMerge = Para [Str "abcdabcd"]++expectedMergeDup :: Block+expectedMergeDup = Para [Str "abcd", Str "abcd"]++-- * Filters++-- ** Plain filters++capFilterInline :: Inline -> Inline+capFilterInline (Str str) = Str $ T.toUpper str+capFilterInline x         = x++uncapFilterInline :: Inline -> Inline+uncapFilterInline (Str str) = Str $ T.toLower str+uncapFilterInline x         = x++unParaFilterBlock :: Block -> Block+unParaFilterBlock (Para ils) = Plain ils+unParaFilterBlock x          = x++dupFilterInline :: Inline -> [Inline]+dupFilterInline (Str str) = [Str str, Str str]+dupFilterInline x         = [x]++mergeFilterInline :: [Inline] -> [Inline]+mergeFilterInline (Str str1 : Str str2 : xs) = Str (str1 <> str2) : xs+mergeFilterInline x                          = x++dupFilterBlock :: Block -> [Block]+dupFilterBlock (Para ils) = [Para ils, Para ils]+dupFilterBlock x          = [x]++-- ** Monadic filters++extractFilterInlineM :: Inline -> Writer Text Inline+extractFilterInlineM il@(Str str) = tell str >> return (capFilterInline il)+extractFilterInlineM x            = return x++extractFilterInlineML :: Inline -> Writer Text [Inline]+extractFilterInlineML il@(Str str) = tell str >> return (dupFilterInline il)+extractFilterInlineML x            = return [x]++extractFilter :: Inline -> Writer Text Inline+extractFilter il@(Str str) = tell str >> return il+extractFilter x            = return x++-- * Readme example++-- ** Filters++behead :: Block -> Block+behead (Header n _ xs) | n >= 2 = Para [Emph xs]+behead x               = x++beheadFilter :: PandocFilter+beheadFilter = mkFilter behead++delink :: Inline -> [Inline]+delink (Link _ txt _) = txt+delink x              = [x]++delinkFilter :: PandocFilter+delinkFilter = mkFilter delink++myFilter :: PandocFilter+myFilter = beheadFilter <> delinkFilter++-- ** Documents++readmeText :: Text+readmeText = T.strip $ T.unlines+  [ "## Heading"+  , "Hello, [Pandoc](https://pandoc.org)."+  ]++readmeDoc :: Pandoc+readmeDoc = Pandoc (Meta mempty)+  [ Header 2 ("heading", [], []) [Str "Heading"]+  , Para [ Str "Hello,"+         , Space+         , Link ("",[],[]) [Str "Pandoc"] ("https://pandoc.org","")+         , Str "."+         ]+  ]++expectedHtml :: Text+expectedHtml = T.strip $ T.unlines+  [ "<p><em>Heading</em></p>"+  , "<p>Hello, Pandoc.</p>"+  ]++expectedDoc :: Pandoc+expectedDoc = Pandoc (Meta mempty)+  [ Para [Emph [Str "Heading"]]+  , Para [ Str "Hello,"+         , Space+         , Str "Pandoc"+         , Str "."+         ]+  ]++-- ** The example++mdToHtml+  :: Text                      -- ^ Input markdown string+  -> Either P.PandocError Text -- ^ Html string or error+mdToHtml md = P.runPure $ do+  doc <- P.readMarkdown def md+  let doc' = applyFilters [beheadFilter, delinkFilter] doc+  P.writeHtml5String def doc'++mdToHtmlCompose+  :: Text                      -- ^ Input markdown string+  -> Either P.PandocError Text -- ^ Html string or error+mdToHtmlCompose md = P.runPure $ do+  doc <- P.readMarkdown def md+  let doc' = applyFilter myFilter doc+  P.writeHtml5String def doc'+++convertSpec :: Spec+convertSpec = parallel $ do+  describe "mkFilter" $ do+    it "converts a -> a filter to Pandoc -> Pandoc filter" $ do+      applyFilter (mkFilter capFilterInline) docPara `shouldBe` expectedInline+      applyFilter (mkFilter unParaFilterBlock) docPara `shouldBe` expectedBlock++    it "converts a -> a filter to b -> b partial filter" $+      applyFilter (mkFilter capFilterInline) blockPara `shouldBe` expectedInlinePartial++    it "converts a -> [a] filter to Pandoc -> Pandoc filter" $ do+      applyFilter (mkFilter dupFilterInline) docPara `shouldBe` expectedInlineL+      applyFilter (mkFilter dupFilterBlock) docPara `shouldBe` expectedBlockL++    it "converts a -> [a] filter to b -> b partial filter" $+      applyFilter (mkFilter dupFilterInline) blockPara `shouldBe` expectedInlinePartialL++    it "converts a -> m a filter to Pandoc -> m Pandoc filter" $ do+      let (doc, s) = runWriter $ applyFilterM (mkFilter extractFilterInlineM) docPara+      doc `shouldBe` expectedInline+      s `shouldBe` T.pack "abcd"++    it "converts a -> m a filter to b -> m b filter" $ do+      let (bl, s) = runWriter $ applyFilterM (mkFilter extractFilterInlineM) blockPara+      bl `shouldBe` expectedInlinePartial+      s `shouldBe` T.pack "abcd"++    it "converts a -> m [a] filter to Pandoc -> m Pandoc filter" $ do+      let (doc, s) = runWriter $ applyFilterM (mkFilter extractFilterInlineML) docPara+      doc `shouldBe` expectedInlineL+      s `shouldBe` T.pack "abcd"++    it "converts a -> m [a] filter to b -> m b filter" $ do+      let (bl, s) = runWriter $ applyFilterM (mkFilter extractFilterInlineML) blockPara+      bl `shouldBe` expectedInlinePartialL+      s `shouldBe` T.pack "abcd"++    it "converts PartialFilter a to PartialFilter b" $ do+      let fInline = mkFilter capFilterInline :: PartialFilter Inline+          fBlock = mkFilter fInline          :: PartialFilter Block+          fPandoc = mkFilter fInline         :: PandocFilter+      applyFilter fBlock blockPara `shouldBe` expectedInlinePartial+      applyFilter fPandoc docPara `shouldBe` expectedInline++    it "converts PartialFilterM m a to PartialFilterM m b" $ do+      let fInline :: PartialFilterM (Writer Text) Inline+          fInline = mkFilter extractFilterInlineM+          fBlock = mkFilter fInline  :: PartialFilterM (Writer Text) Block+          fPandoc = mkFilter fInline :: PandocFilterM (Writer Text)+      let (doc, s) = runWriter $ applyFilterM fPandoc docPara+      doc `shouldBe` expectedInline+      s `shouldBe` T.pack "abcd"++      let (bl, s') = runWriter $ applyFilterM fBlock blockPara+      bl `shouldBe` expectedInlinePartial+      s' `shouldBe` T.pack "abcd"++  describe "getFilter" $ do+    it "converts PartialFilter a to a -> a" $ do+      let fInline = mkFilter capFilterInline :: PartialFilter Inline+      getFilter fInline (Str "abcd") `shouldBe` Str "ABCD"++    it "converts PartialFilterM m a to a -> m a" $ do+      let fInline :: PartialFilterM (Writer Text) Inline+          fInline = mkFilter extractFilterInlineM+      let (doc, s) = runWriter $ getFilterM fInline (Str "abcd")+      doc `shouldBe` Str "ABCD"+      s `shouldBe` T.pack "abcd"++    it "converts PartialFilter a to b -> b" $ do+      let fInline = mkFilter capFilterInline :: PartialFilter Inline+          fBlock = getFilter fInline         :: Block -> Block+          fPandoc = getFilter fInline        :: Pandoc -> Pandoc+      fBlock blockPara `shouldBe` expectedInlinePartial+      fPandoc docPara `shouldBe` expectedInline++    it "converts PartialFilterM m a to b -> m b" $ do+      let fInline :: PartialFilterM (Writer Text) Inline+          fInline = mkFilter extractFilterInlineM+          fBlock  = getFilterM fInline :: Block  -> Writer Text Block+          fPandoc = getFilterM fInline :: Pandoc -> Writer Text Pandoc+      let (doc, s) = runWriter $ fPandoc docPara+      doc `shouldBe` expectedInline+      s `shouldBe` T.pack "abcd"++      let (bl, s') = runWriter $ fBlock blockPara+      bl `shouldBe` expectedInlinePartial+      s' `shouldBe` T.pack "abcd"++  describe "mkConcatedFilter" $ do+    let cap = mkConcatedFilter [uncapFilterInline, capFilterInline]+        uncap = mkConcatedFilter [capFilterInline, uncapFilterInline]+    it "concats filter from left to right" $ do+      applyFilter cap docPara `shouldBe` expectedInline+      applyFilter uncap docPara `shouldBe` docPara++  describe "toFilterM" $+    it "converts a -> a filter to a -> m a filter" $ do+      let capFilterM = toFilterM $ mkFilter capFilterInline+      applyFilterM capFilterM (Str "abcd") `shouldBe` Just (Str "ABCD")++composeSpec :: Spec+composeSpec = parallel $ do+  let dup = mkFilter dupFilterInline+      merge = mkFilter mergeFilterInline+      extract = mkFilter extractFilter+  describe "applyFilter" $ do+    it "applys dup correctly" $+      applyFilter dup compPara `shouldBe` expectedDup++    it "applys merge correctly" $+      applyFilter merge compPara2 `shouldBe` expectedMerge++  describe "applyFilters and monoid" $ do+    it "applys PartialFilter composition from left to right" $ do+      applyFilter (dup <> merge) compPara `shouldBe` expectedDupMerge+      applyFilter (merge <> dup) compPara `shouldBe` expectedMergeDup+      applyFilters [dup, merge] compPara `shouldBe` expectedDupMerge+      applyFilters [merge, dup] compPara `shouldBe` expectedMergeDup++    it "applys PartialFilterM composition from left to right" $ do+      let (doc, s) = runWriter $ applyFilterM (extract <> toFilterM dup) compPara+      doc `shouldBe` expectedDup+      s `shouldBe` T.pack "abcd"++      let (doc', s') = runWriter $ applyFilterM (toFilterM dup <> extract) compPara+      doc' `shouldBe` expectedDup+      s' `shouldBe` T.pack "abcdabcd"++readmeSpec :: Spec+readmeSpec = parallel $+  describe "readme example" $ do+    it "processes filter examples correctly on AST level" $ do+      applyFilters [beheadFilter, delinkFilter] readmeDoc `shouldBe` expectedDoc+      applyFilter myFilter readmeDoc `shouldBe` expectedDoc++    it "processes filter examples correctly on Text level" $ do+      fromRight "" (mdToHtml readmeText) `shouldBe` expectedHtml+      fromRight "" (mdToHtmlCompose readmeText) `shouldBe` expectedHtml++attrBuilderSpec :: Spec+attrBuilderSpec = parallel $ do+  let testAttr = ("id", ["test"], [("k", "v")])+  describe "addClass" $+    it "adds a new class to attributes" $ do+      nullAttr `addClass` "test" `shouldBe` ("", ["test"], [])+      nullAttr `addClass` "test" `addClass` "test2" `shouldBe` ("", ["test2", "test"], [])+      testAttr `addClass` "test2" `shouldBe` ("id", ["test2", "test"], [("k", "v")])++  describe "addClasses" $+    it "adds new classes to attributes" $ do+      nullAttr `addClasses` ["test", "test2"] `shouldBe` ("", ["test", "test2"], [])+      testAttr `addClasses` ["test1", "test2"] `shouldBe` ("id", ["test1", "test2", "test"], [("k", "v")])++  describe "addKVPair" $+    it "adds a new kv pair to attributes" $ do+      nullAttr `addKVPair` ("k", "v") `shouldBe` ("", [], [("k", "v")])+      testAttr `addKVPair` ("k2", "v2") `shouldBe` ("id", ["test"], [("k2", "v2"), ("k", "v")])++  describe "addKVPairs" $+    it "adds new kv pairs to attributes" $ do+      nullAttr `addKVPairs` [("k", "v"), ("k2", "v2")] `shouldBe` ("", [], [("k", "v"), ("k2", "v2")])+      testAttr `addKVPairs` [("k1", "v1"), ("k2", "v2")] `shouldBe` ("id", ["test"], [("k1", "v1"), ("k2", "v2"), ("k", "v")])++  describe "setId" $+    it "sets id of attributes" $ do+      nullAttr `setId` "id" `shouldBe` ("id", [], [])+      testAttr `setId` "id2" `shouldBe` ("id2", ["test"], [("k", "v")])++main :: IO ()+main = do+  testConvert <- testSpec "Filter conversion" convertSpec+  testCompose <- testSpec "Filter composition" composeSpec+  testReadme <- testSpec "Readme examples" readmeSpec+  testAttrBuilder <- testSpec "Attr builder" attrBuilderSpec+  defaultMain $ testGroup "Tests"+    [ testConvert+    , testCompose+    , testReadme+    , testAttrBuilder+    ]