diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
 -------------
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -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
diff --git a/lint/Main.hs b/lint/Main.hs
new file mode 100644
--- /dev/null
+++ b/lint/Main.hs
@@ -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
diff --git a/src/Text/CommonMark/Sub.hs b/src/Text/CommonMark/Sub.hs
--- a/src/Text/CommonMark/Sub.hs
+++ b/src/Text/CommonMark/Sub.hs
@@ -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 =
diff --git a/submark.cabal b/submark.cabal
--- a/submark.cabal
+++ b/submark.cabal
@@ -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
diff --git a/test/HLint.hs b/test/HLint.hs
deleted file mode 100644
--- a/test/HLint.hs
+++ /dev/null
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/Spec.hs b/test/Spec.hs
deleted file mode 100644
--- a/test/Spec.hs
+++ /dev/null
@@ -1,1 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/Text/CommonMark/QQ.hs b/test/Text/CommonMark/QQ.hs
--- a/test/Text/CommonMark/QQ.hs
+++ b/test/Text/CommonMark/QQ.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
 module Text.CommonMark.QQ (doc, node, nodes) where
 
 import CMark
diff --git a/test/Text/CommonMark/SubSpec.hs b/test/Text/CommonMark/SubSpec.hs
--- a/test/Text/CommonMark/SubSpec.hs
+++ b/test/Text/CommonMark/SubSpec.hs
@@ -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.
