diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
 versions in `A.B.C.D` format.  `A` may be incremented arbitrarily for
 non-technical reasons, but [semantic versioning][SemVer] is otherwise
 followed, where `A.B` is the major version, `C` is the minor version, and `D`
-is the patch version.  Initial development uses versions `0.0.0.D`, for which
+is the patch version.  Initial development uses versions `0.0.C.D`, for which
 every version is considered breaking.
 
 [PVP]: <https://pvp.haskell.org/>
@@ -23,6 +23,24 @@
 * Changes are listed in arbitrary order and present tense.
 
 [KaC]: <https://keepachangelog.com/en/1.0.0/>
+
+## 0.4.0.0 (2025-01-03)
+
+### Breaking
+
+* Remove support for GHC 8.6, constraining lower bounds
+* Remove support for GHC 8.4, constraining lower bounds
+* Remove support for GHC 8.2, constraining lower bounds
+* Change minimal Cabal from 1.24 to 3.0
+
+### Non-Breaking
+
+* Bump `base` dependency version upper bound
+* Bump `bytestring` dependency version upper bound
+* Bump `filepath` dependency version upper bound
+* Bump `tasty` dependency version upper bound
+* Bump `text` dependency version upper bound
+* Bump `ttc` dependency version upper bound
 
 ## 0.3.0.0 (2023-05-28)
 
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License
 
-Copyright (c) 2021-2023 Travis Cardwell
+Copyright (c) 2021-2025 Travis Cardwell
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/app/LibOA.hs b/app/LibOA.hs
--- a/app/LibOA.hs
+++ b/app/LibOA.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LibOA
 -- Description : supplementary functions for optparse-applicative
--- Copyright   : Copyright (c) 2019-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2019-2025 Travis Cardwell
 -- License     : MIT
 --
 -- This is a collection of functions that I often use with
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : Main
 -- Description : literatex command-line utility
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 ------------------------------------------------------------------------------
 
diff --git a/literatex.cabal b/literatex.cabal
--- a/literatex.cabal
+++ b/literatex.cabal
@@ -1,36 +1,35 @@
-name:           literatex
-version:        0.3.0.0
-category:       Utils
-synopsis:       transform literate source code to Markdown
+cabal-version:      3.0
+name:               literatex
+version:            0.4.0.0
+synopsis:           transform literate source code to Markdown
 description:
   This package provides a library as well as a command-line utility that
   transforms literate source code to Markdown.  Please see the README on
   GitHub at <https://github.com/ExtremaIS/literatex-haskell#readme>.
+homepage:           https://github.com/ExtremaIS/literatex-haskell#readme
+bug-reports:        https://github.com/ExtremaIS/literatex-haskell/issues
+license:            MIT
+license-file:       LICENSE
+author:             Travis Cardwell <travis.cardwell@extrema.is>
+maintainer:         Travis Cardwell <travis.cardwell@extrema.is>
+copyright:          Copyright (c) 2021-2025 Travis Cardwell
+category:           Utils
+build-type:         Simple
 
-homepage:       https://github.com/ExtremaIS/literatex-haskell#readme
-bug-reports:    https://github.com/ExtremaIS/literatex-haskell/issues
-author:         Travis Cardwell <travis.cardwell@extrema.is>
-maintainer:     Travis Cardwell <travis.cardwell@extrema.is>
-copyright:      Copyright (c) 2021-2023 Travis Cardwell
-license:        MIT
-license-file:   LICENSE
+extra-doc-files:
+  CHANGELOG.md
+  README.md
 
-cabal-version:  1.24
-build-type:     Simple
 tested-with:
-  GHC ==8.2.2
-   || ==8.4.4
-   || ==8.6.5
-   || ==8.8.4
+  GHC ==8.8.4
    || ==8.10.7
    || ==9.0.2
    || ==9.2.8
-   || ==9.4.5
-   || ==9.6.2
-
-extra-source-files:
-  CHANGELOG.md
-  README.md
+   || ==9.4.8
+   || ==9.6.6
+   || ==9.8.4
+   || ==9.10.1
+   || ==9.12.1
 
 source-repository head
   type: git
@@ -60,13 +59,15 @@
     , LiterateX.Types.TargetFormat
   other-modules:
       Paths_literatex
+  autogen-modules:
+      Paths_literatex
   build-depends:
-      base >=4.10.1 && <4.19
-    , bytestring >=0.10.8 && <0.12
-    , conduit >=1.3 && <1.4
-    , text >=1.2.3 && <2.1
-    , ttc >=0.4 && <1.3
-    , unliftio >=0.2 && <0.3
+      base >=4.13.0.0 && <4.22
+    , bytestring >=0.10.10.1 && <0.13
+    , conduit >=1.3.4 && <1.4
+    , text >=1.2.4.0 && <2.2
+    , ttc >=0.4.0.0 && <1.6
+    , unliftio >=0.2.13.1 && <0.3
   default-language: Haskell2010
   default-extensions:
       OverloadedStrings
@@ -87,8 +88,8 @@
       , prettyprinter >=1.7.1 && <1.8
   else
     build-depends:
-        ansi-wl-pprint >=0.6.8 && <1.1
-      , optparse-applicative >=0.13 && <0.18
+        ansi-wl-pprint >=0.6.9 && <1.1
+      , optparse-applicative >=0.15.1.0 && <0.18
   default-language: Haskell2010
   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
 
@@ -135,10 +136,10 @@
   build-depends:
       base
     , bytestring
-    , filepath >=1.4.1.2 && <1.5
+    , filepath >=1.4.2.1 && <1.6
     , literatex
-    , tasty >=0.12 && <1.5
-    , tasty-hunit >=0.8 && <0.11
+    , tasty >=1.2.3 && <1.6
+    , tasty-hunit >=0.10.0.3 && <0.11
     , text
     , ttc
     , unliftio
diff --git a/src/LiterateX.hs b/src/LiterateX.hs
--- a/src/LiterateX.hs
+++ b/src/LiterateX.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX
 -- Description : API
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 --
 -- This module provides high-level as well as low-level API functions for
@@ -315,7 +315,7 @@
 -- * 'sourceLazyText'
 -- * 'sourceByteString'
 -- * 'sourceLazyByteString'
--- * 'sourceHandle'
+-- * 'LiterateX.sourceHandle'
 --
 -- This function works with the following output line consumers:
 --
@@ -324,7 +324,7 @@
 -- * 'sinkLazyText'
 -- * 'sinkByteString'
 -- * 'sinkLazyByteString'
--- * 'sinkHandle'
+-- * 'LiterateX.sinkHandle'
 --
 -- @since 0.0.1.0
 runIO
@@ -346,8 +346,8 @@
 -- * 'sourceLazyText'
 -- * 'sourceByteString'
 -- * 'sourceLazyByteString'
--- * 'sourceHandle'
--- * 'sourceFile'
+-- * 'LiterateX.sourceHandle'
+-- * 'LiterateX.sourceFile'
 --
 -- This function works with the following output line consumers:
 --
@@ -356,8 +356,8 @@
 -- * 'sinkLazyText'
 -- * 'sinkByteString'
 -- * 'sinkLazyByteString'
--- * 'sinkHandle'
--- * 'sinkFile'
+-- * 'LiterateX.sinkHandle'
+-- * 'LiterateX.sinkFile'
 --
 -- @since 0.0.1.0
 runResource
diff --git a/src/LiterateX/Parser.hs b/src/LiterateX/Parser.hs
--- a/src/LiterateX/Parser.hs
+++ b/src/LiterateX/Parser.hs
@@ -2,24 +2,20 @@
 -- |
 -- Module      : LiterateX.Parser
 -- Description : source parser
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 --
 -- This module implements the source parser.
 ------------------------------------------------------------------------------
 
 {-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module LiterateX.Parser
   ( -- * API
     parse
   ) where
 
--- https://hackage.haskell.org/package/base
-import Control.Monad (guard)
-import Data.Maybe (fromMaybe)
-
 -- https://hackage.haskell.org/package/conduit
 import qualified Data.Conduit as C
 import Data.Conduit (ConduitT)
@@ -46,148 +42,77 @@
   :: Monad m
   => SourceFormat
   -> ConduitT Text SourceLine m ()
-parse = parseSourceLines . parserFunctionsFor
+parse sourceFormat = do
+    let parseLine' = parseLine sourceFormat
+    mLine <- C.await
+    case mLine of
+      Just line -> do
+        C.yield $ if "#!" `T.isPrefixOf` line
+          then SourceLine.Shebang line
+          else parseLine' line
+        C.awaitForever $ C.yield . parseLine'
+      Nothing -> return ()
 
 ------------------------------------------------------------------------------
 -- $Internal
 
--- | Parser functions that determine how input is parsed
-data ParserFunctions
-  = ParserFunctions
-    { isCodeBlank :: !(Text -> Bool)
-    , isDocBlank  :: !(Text -> Bool)
-    , isRule      :: !(Text -> Bool)
-    , getDoc      :: !(Text -> Maybe Text)
-    , getCode     :: !(Text -> Text)
-    }
-
-------------------------------------------------------------------------------
-
--- | Get the parser functions for the specified source format
-parserFunctionsFor :: SourceFormat -> ParserFunctions
-parserFunctionsFor = \case
-    SourceFormat.DoubleDash      -> lineCommentParserFunctions '-' 2
-    SourceFormat.DoubleSlash     -> lineCommentParserFunctions '/' 2
-    SourceFormat.Hash            -> lineCommentParserFunctions '#' 1
-    SourceFormat.LiterateHaskell -> literateHaskellParserFunctions
-    SourceFormat.Percent         -> lineCommentParserFunctions '%' 1
-    SourceFormat.LispSemicolons  -> lispCommentParserFunctions
+-- | Parse a source line according to the source format
+parseLine :: SourceFormat -> Text -> SourceLine
+parseLine = \case
+    SourceFormat.DoubleDash      -> parseLineCommentLine '-' 2
+    SourceFormat.DoubleSlash     -> parseLineCommentLine '/' 2
+    SourceFormat.Hash            -> parseLineCommentLine '#' 1
+    SourceFormat.LiterateHaskell -> parseLiterateHaskellLine
+    SourceFormat.Percent         -> parseLineCommentLine '%' 1
+    SourceFormat.LispSemicolons  -> parseLispCommentLine
 
 ------------------------------------------------------------------------------
 
--- | Get parser functions for source with line-based comments
-lineCommentParserFunctions
+-- | Parse a source line using line-based comments
+parseLineCommentLine
   :: Char  -- ^ comment character
   -> Int   -- ^ number of comment characters to create line comment
-  -> ParserFunctions
-lineCommentParserFunctions char count = ParserFunctions{..}
-  where
-    docBlank :: Text
-    docBlank = T.pack $ replicate count char
-
-    prefixLen :: Int
-    prefixLen = count + 1
-
-    prefix :: Text
-    prefix = T.pack $ replicate count char ++ " "
-
-    isCodeBlank :: Text -> Bool
-    isCodeBlank = T.null
-
-    isDocBlank :: Text -> Bool
-    isDocBlank = (== docBlank)
-
-    isRule :: Text -> Bool
-    isRule line = T.length line > count && T.all (== char) line
-
-    getDoc :: Text -> Maybe Text
-    getDoc line = do
-      let (linePrefix, lineSuffix) = T.splitAt prefixLen line
-      guard $ linePrefix == prefix
-      pure lineSuffix
-
-    getCode :: Text -> Text
-    getCode = id
+  -> Text  -- ^ source line
+  -> SourceLine
+parseLineCommentLine char count line
+    | T.null line = SourceLine.CodeBlank
+    | otherwise = case T.uncons <$> T.span (== char) line of
+        ("", _) -> SourceLine.Code line
+        (_, Nothing) -> case T.compareLength line count of
+          EQ -> SourceLine.DocBlank
+          GT -> SourceLine.Rule
+          LT -> SourceLine.Code line
+        (l, Just (' ', r)) | T.compareLength l count == EQ -> SourceLine.Doc r
+        _otherwise -> SourceLine.Code line
 
 ------------------------------------------------------------------------------
 
--- | Get parser functions for source with Lisp-style comments
+-- | Parse a source line using Lisp-style comments
 --
 -- Lisp-style comments begin with one or more semicolons.
-lispCommentParserFunctions :: ParserFunctions
-lispCommentParserFunctions = ParserFunctions{..}
-  where
-    isCodeBlank :: Text -> Bool
-    isCodeBlank = T.null
-
-    isDocBlank :: Text -> Bool
-    isDocBlank line =
-      let len = T.length line
-      in  len >= 1 && len <= 4 && T.all (== ';') line
-
-    isRule :: Text -> Bool
-    isRule line = T.length line > 4 && T.all (== ';') line
-
-    getDoc :: Text -> Maybe Text
-    getDoc line = do
-      let (linePrefix, (sep, lineSuffix)) = T.splitAt 1 <$> T.breakOn " " line
-      guard $ not (T.null linePrefix) && T.all (== ';') linePrefix
-      guard $ sep == " "
-      pure lineSuffix
-
-    getCode :: Text -> Text
-    getCode = id
-
-------------------------------------------------------------------------------
-
--- | Get parser functions for parsing literate Haskell
-literateHaskellParserFunctions :: ParserFunctions
-literateHaskellParserFunctions = ParserFunctions{..}
-  where
-    isCodeBlank :: Text -> Bool
-    isCodeBlank = (== ">")
-
-    isDocBlank :: Text -> Bool
-    isDocBlank = T.null
-
-    isRule :: Text -> Bool
-    isRule = const False
-
-    getDoc :: Text -> Maybe Text
-    getDoc line
-      | "> " `T.isPrefixOf` line = Nothing
-      | otherwise                = Just line
-
-    getCode :: Text -> Text
-    getCode line = fromMaybe line $ T.stripPrefix "> " line
+parseLispCommentLine
+  :: Text  -- ^ source line
+  -> SourceLine
+parseLispCommentLine line
+    | T.null line = SourceLine.CodeBlank
+    | otherwise = case T.uncons <$> T.span (== ';') line of
+        ("", _) -> SourceLine.Code line
+        (_, Nothing)
+          | T.compareLength line 4 == GT -> SourceLine.Rule
+          | otherwise -> SourceLine.DocBlank
+        (_, Just (' ', r)) -> SourceLine.Doc r
+        _otherwise -> SourceLine.Code line
 
 ------------------------------------------------------------------------------
 
--- | Create a "Conduit" transformer for the specified parser functions
---
--- This function produces a 'SourceLine' for each line of input.  A
--- 'SourceLine.Shebang' can only be produced on the first line.  Note that the
--- order that the parser functions are used is significant; the parser
--- functions are written for this order.
-parseSourceLines
-  :: Monad m
-  => ParserFunctions
-  -> ConduitT Text SourceLine m ()
-parseSourceLines ParserFunctions{..} = do
-    mLine <- C.await
-    case mLine of
-      Just line -> do
-        C.yield $ if "#!" `T.isPrefixOf` line
-          then SourceLine.Shebang line
-          else parse' line
-        C.awaitForever $ C.yield . parse'
-      Nothing -> return ()
-  where
-    parse' :: Text -> SourceLine
-    parse' line
-      | isCodeBlank line = SourceLine.CodeBlank
-      | isDocBlank line  = SourceLine.DocBlank
-      | isRule line      = SourceLine.Rule
-      | otherwise        = case (getDoc line, getCode line) of
-          (Just doc, _code) -> SourceLine.Doc doc
-          (Nothing,  code)  -> SourceLine.Code code
+-- | Parse a Literate Haskell source line
+parseLiterateHaskellLine
+  :: Text  -- ^ source line
+  -> SourceLine
+parseLiterateHaskellLine line = case T.uncons line of
+    Nothing -> SourceLine.DocBlank
+    Just ('>', r1) -> case T.uncons r1 of
+      Nothing -> SourceLine.CodeBlank
+      Just (' ', r2) -> SourceLine.Code r2
+      _otherwise -> SourceLine.Doc line
+    _otherwise -> SourceLine.Doc line
diff --git a/src/LiterateX/Renderer.hs b/src/LiterateX/Renderer.hs
--- a/src/LiterateX/Renderer.hs
+++ b/src/LiterateX/Renderer.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX.Renderer
 -- Description : target renderer
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 --
 -- This module implements the target renderer.
diff --git a/src/LiterateX/SourceDefaults.hs b/src/LiterateX/SourceDefaults.hs
--- a/src/LiterateX/SourceDefaults.hs
+++ b/src/LiterateX/SourceDefaults.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX.SourceDefaults
 -- Description : default options by source extension
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 --
 -- This module provides some default options for various sources.
@@ -29,7 +29,7 @@
 -- @since 0.0.1.0
 defaultsFor :: FilePath -> Maybe (SourceFormat, CodeLanguage)
 defaultsFor path =
-  fmap snd . flip find extensionDefaults $ flip isSuffixOf path . fst
+    fmap snd . flip find extensionDefaults $ flip isSuffixOf path . fst
 
 ------------------------------------------------------------------------------
 
diff --git a/src/LiterateX/Types.hs b/src/LiterateX/Types.hs
--- a/src/LiterateX/Types.hs
+++ b/src/LiterateX/Types.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX.Types
 -- Description : type re-exports for convenience
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 --
 -- The type modules are generally imported qualified, so the types are
diff --git a/src/LiterateX/Types/CodeLanguage.hs b/src/LiterateX/Types/CodeLanguage.hs
--- a/src/LiterateX/Types/CodeLanguage.hs
+++ b/src/LiterateX/Types/CodeLanguage.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX.Types.CodeLanguage
 -- Description : source code language type
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 ------------------------------------------------------------------------------
 
diff --git a/src/LiterateX/Types/SourceFormat.hs b/src/LiterateX/Types/SourceFormat.hs
--- a/src/LiterateX/Types/SourceFormat.hs
+++ b/src/LiterateX/Types/SourceFormat.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX.Types.SourceFormat
 -- Description : source format type
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 ------------------------------------------------------------------------------
 
diff --git a/src/LiterateX/Types/SourceLine.hs b/src/LiterateX/Types/SourceLine.hs
--- a/src/LiterateX/Types/SourceLine.hs
+++ b/src/LiterateX/Types/SourceLine.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX.Types.SourceLine
 -- Description : source line type
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 ------------------------------------------------------------------------------
 
diff --git a/src/LiterateX/Types/TargetFormat.hs b/src/LiterateX/Types/TargetFormat.hs
--- a/src/LiterateX/Types/TargetFormat.hs
+++ b/src/LiterateX/Types/TargetFormat.hs
@@ -2,7 +2,7 @@
 -- |
 -- Module      : LiterateX.Types.TargetFormat
 -- Description : target format type
--- Copyright   : Copyright (c) 2021-2023 Travis Cardwell
+-- Copyright   : Copyright (c) 2021-2025 Travis Cardwell
 -- License     : MIT
 ------------------------------------------------------------------------------
 
