packages feed

submark 0.2.0 → 0.3.0

raw patch · 11 files changed

+435/−117 lines, 11 filesdep +regex-tdfadep ~hlintdep ~optparse-applicative

Dependencies added: regex-tdfa

Dependency ranges changed: hlint, optparse-applicative

Files

CHANGELOG.md view
@@ -1,5 +1,23 @@-`subtoml` changelog+`submark` changelog ===================++Version 0.3.0+-------------++Released on March 11, 2022.++ -  Added options from `--h1-regex` to `--h6-regex`.  [[#2]]+ -  Added `Text.CommonMark.Sub.HeadingPattern` type.  [[#2]]+ -  Added `Text.CommonMark.Sub.HeadingTitlePattern` type.  [[#2]]+ -  The signature of `Text.CommonMark.Sub.extractSection` function was changed+    from `Level -> (Text -> Text -> Bool) -> Text -> Node -> Node` to+    `HeadingPattern -> Node -> Node`.  [[#2]]+ -  The signature of `Text.CommonMark.Sub.matchesHeading` function was changed+    from `Level -> (Text -> Text -> Bool) -> Text -> Node -> Bool` to+    `HeadingPattern -> Node -> Bool`.  [[#2]]++[#2]: https://github.com/dahlia/submark/issues/2+  Version 0.2.0 -------------
README.md view
@@ -1,17 +1,17 @@ `submark`: Extract a part from CommonMark/Markdown docs ======================================================= -[![CircleCI][circleci-badge]][circleci]+[![GitHub Actions][gh-actions-badge]][gh-actions] [![Hackage][hackage-badge]][hackage]  `submark` is a CLI program to extract some particular section from a given CommonMark/Markdown document.  I use it for myself to extract-the latest version section from the CHANGELOG.md file, and then reuse the text+the latest version section from the *CHANGELOG.md* file, and then reuse the text for the corresponding release note on GitHub releases, during automated release process which is run on CI. -[circleci-badge]: https://circleci.com/gh/dahlia/submark.svg?style=shield-[circleci]: https://circleci.com/gh/dahlia/submark+[gh-actions-badge]: https://github.com/dahlia/submark/actions/workflows/build.yaml/badge.svg+[gh-actions]: https://github.com/dahlia/submark/actions/workflows/build.yaml [hackage-badge]: https://img.shields.io/hackage/v/submark.svg [hackage]: https://hackage.haskell.org/package/submark @@ -19,22 +19,21 @@ Download & installation ----------------------- -For Linux x86_64, executable binaries are available on [GitHub releases][].-Each file is a single executable, and statically linked so that it's executable-as a standalone without dependencies.+Prebuilt binaries for the following platforms and architectures are available on+[GitHub releases]: -For other platforms, you need to build it by yourself.  It's written in Haskell,-so you need to install [Haskell Stack][] first.  It can be built in the same-way other Haskell programs are:+ -  Linux (x86_64)+ -  macOS (x86_64)+ -  Windows (win64) +For other platforms and architectures, you need to build it by yourself.+It's written in Haskell, so you need to install [Haskell Stack] first.+It can be built in the same way other Haskell programs are:+ ~~~~~~~~ bash $ stack setup && stack install ~~~~~~~~ -I'm going to officially support executable binaries for other platforms-if anyone asks.  (I thought `submark` wouldn't be useful on other than Linux-since the most of CI machines are Linux, but it might be wrong.)- [GitHub releases]: https://github.com/dahlia/submark/releases [Haskell Stack]: https://haskellstack.org/ @@ -78,6 +77,17 @@ (18 KB) -- 17 Dec 2004 ~~~~~~~~ +Options from `--h1-regex` to `--h6-regex` take a regular expression instead of+an exact heading text:++~~~~~~~~ bash+$ submark --h2-regex "^(Down|Up)load$" index.text+## Download++[Markdown 1.0.1](http://daringfireball.net/projects/downloads/Markdown_1.0.1.zip)+(18 KB) -- 17 Dec 2004+~~~~~~~~+ The leading heading can be omitted:  ~~~~~~~~ bash@@ -99,7 +109,7 @@ (18 KB) -- 17 Dec 2004 ~~~~~~~~ -By Unix convention, `-` means pipe: +By Unix convention, `-` means pipe:  ~~~~~~~~ bash $ submark --h2 "Download" - < index.text@@ -116,3 +126,11 @@ ~~~~~~~~  [1]: https://daringfireball.net/projects/markdown/index.text+++Software license+----------------++This software is distributed under [GPL 3].++[GPL 3]: https://www.gnu.org/licenses/gpl-3.0.html
app/Main.hs view
@@ -1,5 +1,6 @@+{-# LANGUAGE DisambiguateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-} import Data.Maybe-import Data.Semigroup ((<>)) import System.IO  import CMark@@ -9,9 +10,7 @@  import Text.CommonMark.Sub -data HeadingPattern = HeadingPattern Level Text deriving (Eq, Ord, Show)--data Extraction = Extraction +data Extraction = Extraction     { outputFilePath :: FilePath     , headingPattern :: HeadingPattern     , ignoreCase :: Bool@@ -20,54 +19,74 @@     , inputFilePath :: FilePath     } deriving (Eq, Ord, Show) -headingLevel :: Parser HeadingPattern-headingLevel =+headingPatternParser :: Parser HeadingPattern+headingPatternParser =     foldl (<|>) empty parsers   where     levels :: [Level]     levels = [1..6]     parsers :: [Parser HeadingPattern]-    parsers =-        [ (HeadingPattern l . pack) <$> strOption+    parsers = titleTextParsers ++ titleRegexParsers+    titleTextParsers :: [Parser HeadingPattern]+    titleTextParsers =+        [ HeadingPattern l False . HeadingTitleText . pack <$> strOption               (  long ("h" ++ show l)               <> metavar "TITLE"               <> help ("Extract the section with the heading level " ++-                       show l ++ ".  Mutually exclusive with other --h* " ++-                       "options")+                       show l ++ ".  Note that it tries to match to the " +++                       "heading title with no markup, which means " +++                       "--h1 \"foo bar\" matches to both `# foo bar' and " +++                       "`# _foo_ **bar**'.  Mutually exclusive with other " +++                       "--h* and --h*-regex options.")               )         | l <- levels         ]+    titleRegexParsers :: [Parser HeadingPattern]+    titleRegexParsers =+        [ HeadingPattern l False . HeadingTitleRegex . pack <$> strOption+              (  long ("h" ++ show l ++ "-regex")+              <> metavar "PATTERN"+              <> help ("Similar to --h" ++ show l ++ " except that it takes " +++                       "a regular expression.  Note that it tries to match " +++                       "to the heading title with no markup, which means " +++                       "--h1 \"fo+ ba[rz]\" matches to both `# foo bar' and " +++                       "`# _foooo_ **baz**'.  Mutually exclusive with other " +++                       "--h* and --h*-regex options.")+              )+        | l <- levels+        ]  parser :: Parser Extraction parser = Extraction+    -- CHECK: Write docs in README.md for a new option.     <$> strOption (  long "out-file"                   <> short 'o'                   <> metavar "FILE"                   <> value "-"                   <> showDefault-                  <> help "Write output to FILE"+                  <> help "Write output to FILE."                   )-    <*> headingLevel+    <*> headingPatternParser     <*> switch (  long "ignore-case"                <> short 'i'-               <> help "Ignore case distinctions"+               <> help "Ignore case distinctions."                )     <*> switch (  long "omit-heading"                <> short 'O'-               <> help "Omit a leading heading"+               <> help "Omit a leading heading."                )     <*> ( Just . read <$> strOption             (  long "columns"             <> short 'c'             <> metavar "WIDTH"             <> help ("Limit the maximum characters per line of the output.  " ++-                     "No limit by default")+                     "No limit by default.")             )         <|> pure Nothing         )     <**> helper     <*> strArgument (  metavar "FILE"-                    <> help "CommonMark/Markdown text to extract from"+                    <> help "CommonMark/Markdown text to extract from."                     )  parserInfo :: ParserInfo Extraction@@ -93,21 +112,19 @@     withFile o WriteMode action'  extract :: Extraction -> IO ()-extract e@Extraction { headingPattern = (HeadingPattern level title)-                     , ignoreCase = ignoreCase'-                     , omitHeading = omitHeading'-                     , columnWidth = width+extract e@Extraction { headingPattern+                     , ignoreCase+                     , omitHeading+                     , columnWidth                      } = do     text <- withInputFile e TIO.hGetContents     let doc = commonmarkToNode [] text-        node = case (omitHeading', extractSection level equals title doc) of+        pat = headingPattern { caseInsensitive = ignoreCase }+        node = case (omitHeading, extractSection pat doc) of             (True, Node p DOCUMENT (_ : xs)) -> Node p DOCUMENT xs             (_, other) -> other-        result = nodeToCommonmark [] (Just $ fromMaybe (-1) width) node+        result = nodeToCommonmark [] (Just $ fromMaybe (-1) columnWidth) node     withOutputFile e (`TIO.hPutStrLn` result)-  where-    equals :: Text -> Text -> Bool-    equals = if ignoreCase' then (\ a b -> toLower a == toLower b) else (==)  main :: IO () main = execParser parserInfo >>= extract
+ lint/Main.hs view
@@ -0,0 +1,12 @@+import Language.Haskell.HLint+import System.Exit++arguments :: [String]+arguments = ["app", "src", "test"]++main :: IO ()+main = do+    hlints <- hlint arguments+    case hlints of+        [] -> exitSuccess+        _ -> exitFailure
src/Text/CommonMark/Sub.hs view
@@ -1,5 +1,10 @@+{-# LANGUAGE DisambiguateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-} module Text.CommonMark.Sub-    ( Level+    ( HeadingPattern (..)+    , HeadingTitlePattern (..)+    , Level     , extractSection     , flattenInlineNodes     , matchesHeading@@ -8,26 +13,71 @@ import Prelude hiding (concat)  import CMark-import Data.Text (Text, concat, pack, strip)+import Data.Text (Text, concat, pack, strip, toLower)+import Text.Regex.TDFA+import Text.Regex.TDFA.Text -extractSection :: Level -> (Text -> Text -> Bool) -> Text -> Node -> Node-extractSection headingLevel equals headingTitle (Node pos DOCUMENT nodes) =+data HeadingPattern = HeadingPattern+    { headingLevel :: Level+    , caseInsensitive :: Bool+    , titlePattern :: HeadingTitlePattern+    } deriving (Eq, Ord, Show)++data HeadingTitlePattern+    = HeadingTitleRegex Text+    | HeadingTitleText Text+    deriving (Eq, Ord, Show)++extractSection :: HeadingPattern -> Node -> Node+extractSection pat@HeadingPattern { headingLevel } (Node pos DOCUMENT nodes) =     Node pos DOCUMENT $ case headlessNodes of-        (x : xs) -> x : takeWhile (isInSection headingLevel) xs+        (x : xs) -> x : takeWhile isInSection xs         [] -> []   where-    isInSection :: Level -> Node -> Bool-    isInSection level (Node _ (HEADING lv) _) = lv > level-    isInSection _ _ = True+    isInSection :: Node -> Bool+    isInSection (Node _ (HEADING lv) _) = lv > headingLevel+    isInSection _ = True     headlessNodes :: [Node]     headlessNodes =-        dropWhile (not . matchesHeading headingLevel equals headingTitle) nodes-extractSection _ _ _ (Node pos _ _) = Node pos DOCUMENT []+        dropWhile (not . matchesHeading pat) nodes+extractSection _ (Node pos _ _) = Node pos DOCUMENT [] -matchesHeading :: Level -> (Text -> Text -> Bool) -> Text -> Node -> Bool-matchesHeading level equals text (Node _ (HEADING lv) nodes) =-    lv == level && flattenInlineNodes nodes `equals` text-matchesHeading _ _ _ _ = False+matchesHeading :: HeadingPattern -> Node -> Bool+matchesHeading HeadingPattern { headingLevel+                              , caseInsensitive+                              , titlePattern+                              } = case titlePattern of+    HeadingTitleText text -> if caseInsensitive+        then \ case+            (Node _ (HEADING lv) nodes) ->+                lv == headingLevel &&+                    toLower (flattenInlineNodes nodes) == toLower text+            _ -> False+        else \ case+            (Node _ (HEADING lv) nodes) ->+                lv == headingLevel && flattenInlineNodes nodes == text+            _ -> False+    HeadingTitleRegex regexPat ->+        let compOption = CompOption+                { caseSensitive = not caseInsensitive+                , multiline = False+                , rightAssoc = True+                , newSyntax = True+                , lastStarGreedy = True+                }+            execOption = ExecOption { captureGroups = False }+            regex' = compile compOption execOption regexPat+        in+            case regex' of+                Left _ -> const False+                Right regex ->+                    \ case+                    (Node _ (HEADING lv) nodes) ->+                        lv == headingLevel &&+                            case execute regex (flattenInlineNodes nodes) of+                                Right (Just _) -> True+                                _ -> False+                    _ -> False  flattenInlineNodes :: [Node] -> Text flattenInlineNodes =
submark.cabal view
@@ -1,13 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack------ hash: 44a3cc6b647f89b49ca4596f50d12a252fc1f579338fb309629f51bb503696f7  name:           submark-version:        0.2.0+version:        0.3.0 synopsis:       Extract a part from CommonMark/Markdown docs category:       Text stability:      alpha@@ -39,10 +37,11 @@       Paths_submark   hs-source-dirs:       src-  ghc-options: -fwarn-incomplete-uni-patterns+  ghc-options: -Wall -fwarn-incomplete-uni-patterns   build-depends:       base >=4.7 && <5     , cmark >=0.5.6 && <0.6.0+    , regex-tdfa >=1.3.1.2 && <1.4     , text ==1.*   default-language: Haskell2010 @@ -52,48 +51,44 @@       Paths_submark   hs-source-dirs:       app-  ghc-options: -fwarn-incomplete-uni-patterns+  ghc-options: -Wall -fwarn-incomplete-uni-patterns   build-depends:       base >=4.7 && <5     , cmark >=0.5.6 && <0.6.0-    , optparse-applicative >=0.13.2.0 && <0.15.0.0+    , optparse-applicative >=0.13.2.0 && <0.17.0.0     , submark     , text ==1.*   if flag(static)-    ghc-options: -fwarn-incomplete-uni-patterns -static -optl-static -optl-pthread -optc-Os -fPIC+    ghc-options: -Wall -fwarn-incomplete-uni-patterns -static -optl-static -optl-pthread -optc-Os -fPIC   else-    ghc-options: -fwarn-incomplete-uni-patterns+    ghc-options: -Wall -fwarn-incomplete-uni-patterns   default-language: Haskell2010  test-suite hlint   type: exitcode-stdio-1.0-  main-is: HLint.hs+  main-is: Main.hs   other-modules:-      Spec-      Text.CommonMark.QQ-      Text.CommonMark.SubSpec       Paths_submark   hs-source-dirs:-      test-  ghc-options: -fwarn-incomplete-uni-patterns+      lint+  ghc-options: -Wall -fwarn-incomplete-uni-patterns   build-depends:       base >=4.7 && <5     , cmark >=0.5.6 && <0.6.0-    , hlint >=2.0.9 && <3+    , hlint >=2.0.9 && <4     , text ==1.*   default-language: Haskell2010  test-suite spec   type: exitcode-stdio-1.0-  main-is: Spec.hs+  main-is: Main.hs   other-modules:-      HLint       Text.CommonMark.QQ       Text.CommonMark.SubSpec       Paths_submark   hs-source-dirs:       test-  ghc-options: -fwarn-incomplete-uni-patterns+  ghc-options: -Wall -fwarn-incomplete-uni-patterns   build-depends:       base >=4.7 && <5     , cmark >=0.5.6 && <0.6.0
− test/HLint.hs
@@ -1,12 +0,0 @@-import Language.Haskell.HLint-import System.Exit--arguments :: [String]-arguments = ["app", "src", "test"]--main :: IO ()-main = do-    hlints <- hlint arguments-    case hlints of-        [] -> exitSuccess-        _ -> exitFailure
+ test/Main.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
− test/Spec.hs
@@ -1,1 +0,0 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
test/Text/CommonMark/QQ.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TemplateHaskellQuotes #-} module Text.CommonMark.QQ (doc, node, nodes) where  import CMark
test/Text/CommonMark/SubSpec.hs view
@@ -2,16 +2,12 @@ {-# LANGUAGE QuasiQuotes #-} module Text.CommonMark.SubSpec (spec) where -import Data.Text import Test.Hspec  import CMark (Node) import Text.CommonMark.QQ import Text.CommonMark.Sub -ciEq :: Text -> Text -> Bool-ciEq t t' = toLower t == toLower t'- source :: Node source = [doc|Doc title =======================@@ -65,31 +61,255 @@  spec :: Spec spec = do-    specify "extractSection" $ do-        extractSection 2 (==) "Section to extract" source `shouldBe` expected-        extractSection 3 (==) "Section to extract" source `shouldNotBe` expected-        extractSection 2 (==) "section to extract" source `shouldNotBe` expected-        extractSection 3 (==) "section to extract" source `shouldNotBe` expected-        -- Case insensitivity-        extractSection 2 ciEq "Section to extract" source `shouldBe` expected-        extractSection 3 ciEq "Section to extract" source `shouldNotBe` expected-        extractSection 2 ciEq "section to extract" source `shouldBe` expected-        extractSection 3 ciEq "section to extract" source `shouldNotBe` expected-    specify "matchesHeading" $ do-        [node|# Title|] `shouldSatisfy` matchesHeading 1 (==) "Title"-        [node|### Title|] `shouldSatisfy` matchesHeading 3 (==) "Title"-        [node|# Other title|] `shouldSatisfy`-            matchesHeading 1 (==) "Other title"-        [node|# Complex *title*|] `shouldSatisfy`-            matchesHeading 1 (==) "Complex title"-        [node|# Title|] `shouldNotSatisfy` matchesHeading 2 (==) "Title"-        [node|# Title|] `shouldNotSatisfy` matchesHeading 1 (==) "Wrong title"-        [node|# Title|] `shouldNotSatisfy` matchesHeading 1 (==) "title"-        -- Case insensitivity-        [node|# Title|] `shouldNotSatisfy` matchesHeading 1 (==) "title"-        [node|# Title|] `shouldSatisfy` matchesHeading 1 ciEq "title"-        [node|# Title|] `shouldNotSatisfy` matchesHeading 2 (==) "title"-        [node|# Title|] `shouldNotSatisfy` matchesHeading 2 ciEq "title"+    describe "extractSection" $ do+        specify "w/ HeadingTitleText" $ do+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Section to extract"+                    }+                source+                `shouldBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Section to extract"+                    }+                source+                `shouldNotBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "section to extract"+                    }+                source+                `shouldNotBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "section to extract"+                    }+                source+                `shouldNotBe` expected+            -- Case insensitivity+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleText "Section to extract"+                    }+                source+                `shouldBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleText "Section to extract"+                    }+                source+                `shouldNotBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleText "section to extract"+                    }+                source+                `shouldBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleText "section to extract"+                    }+                source+                `shouldNotBe` expected+        specify "w/ HeadingTitleRegex" $ do+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "to[[:space:]]+extract$"+                    }+                source+                `shouldBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "to[[:space:]]+extract$"+                    }+                source+                `shouldNotBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "TO[[:space:]]+EXTRACT$"+                    }+                source+                `shouldNotBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "TO[[:space:]]+EXTRACT$"+                    }+                source+                `shouldNotBe` expected+            -- Case insensitivity+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleRegex "to[[:space:]]+extract$"+                    }+                source+                `shouldBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleRegex "to[[:space:]]+extract$"+                    }+                source+                `shouldNotBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleRegex "TO[[:space:]]+EXTRACT$"+                    }+                source+                `shouldBe` expected+            extractSection+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleRegex "TO[[:space:]]+EXTRACT$"+                    }+                source+                `shouldNotBe` expected+    describe "matchesHeading" $ do+        specify "w/ HeadingTitleText" $ do+            [node|# Title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Title"+                    }+            [node|### Title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Title"+                    }+            [node|# Other title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Other title"+                    }+            [node|# Complex *title*|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Complex title"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Title"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "Wrong title"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleText "title"+                    }+            -- Case insensitivity+            [node|# Title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleText "title"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleText "title"+                    }+        specify "w/ HeadingTitleRegex" $ do+            [node|# Title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "^Title$"+                    }+            [node|### Title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 3+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "^Title$"+                    }+            [node|# Other title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern =+                        HeadingTitleRegex "^Other[[:space:]]+title$"+                    }+            [node|# Complex *title*|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern =+                        HeadingTitleRegex "^Complex[[:space:]]+title$"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "^Title$"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern =+                        HeadingTitleRegex "^Wrong[[:space:]]+title$"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = False+                    , titlePattern = HeadingTitleRegex "$title$"+                    }+            -- Case insensitivity+            [node|# Title|] `shouldSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 1+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleRegex "^title$"+                    }+            [node|# Title|] `shouldNotSatisfy` matchesHeading+                HeadingPattern+                    { headingLevel = 2+                    , caseInsensitive = True+                    , titlePattern = HeadingTitleRegex "^title$"+                    }     specify "flattenInlineNodes" $ do         flattenInlineNodes [nodes|*Test* nodes.|] `shouldBe` "Test nodes."         flattenInlineNodes [nodes|Testing multiple paragraphs.