diff --git a/.gitignore b/.gitignore
deleted file mode 100644
--- a/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-dist/
-.cabal-sandbox/
-cabal.sandbox.config
-*.o
-*.hi
-.stack-work
-tests/pretty/*.out
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 language-bash
 -------------
 
-[![Build Status](https://travis-ci.org/knrafto/language-bash.svg)](https://travis-ci.org/knrafto/language-bash)
+![Build Status](https://github.com/knrafto/language-bash/actions/workflows/main.yml/badge.svg)
 
 A library for parsing and pretty-printing Bash shell scripts.
diff --git a/language-bash.cabal b/language-bash.cabal
--- a/language-bash.cabal
+++ b/language-bash.cabal
@@ -1,33 +1,33 @@
+cabal-version:      2.2
 name:               language-bash
-version:            0.9.2
+version:            0.11.1
 category:           Language
-license:            BSD3
+license:            BSD-3-Clause
 license-file:       LICENSE
 author:             Kyle Raftogianis
 maintainer:         Kyle Raftogianis <knrafto@gmail.com>
-copyright:          Copyright (c) 2013-2020 Kyle Raftogianis
+copyright:          Copyright (c) 2013-2025 Kyle Raftogianis
 build-type:         Simple
-cabal-version:      >= 1.8
 homepage:           http://github.com/knrafto/language-bash/
 bug-reports:        http://github.com/knrafto/language-bash/issues
-tested-with:        GHC == 7.10.1, GHC == 7.10.2, GHC == 8.0.2, GHC == 8.4.3, GHC == 8.8.3
 synopsis:           Parsing and pretty-printing Bash shell scripts
 description:
     A library for parsing, pretty-printing, and manipulating
     Bash shell scripts.
 
+tested-with:        GHC == 8.10.7, GHC == 9.0.2, GHC == 9.2.8, GHC == 9.4.8, GHC == 9.6.6, GHC == 9.8.4, GHC == 9.10.1, GHC == 9.12.1
+
 extra-source-files:
-  .gitignore
   README.md
   tests/pretty/*.golden
-  tests/pretty/*.out
   tests/pretty/*.sh
 
 source-repository head
   type: git
-  location: git://github.com/knrafto/language-bash.git
+  location: https://github.com/knrafto/language-bash.git
 
 library
+  default-language: Haskell2010
   hs-source-dirs: src
 
   exposed-modules:
@@ -45,14 +45,22 @@
     Language.Bash.Parse.Internal
 
   build-depends:
-    base          >= 4.6 && < 5,
-    parsec        >= 3.0 && < 4.0,
-    prettyprinter >= 1.2 && < 2.0,
-    transformers  >= 0.2 && < 0.6
+    -- Notes on version bounds:
+    --
+    -- * base-4.11 is the first where Prelude re-exports Semigroup(s). This
+    --   version of base is bundled with 8.4.1.
+    -- * parsec-3.1.17.0 is the first where Text.Parsec.Prim.many1 is defined.
+    -- * prettyprinter-1.7.0 is the first version where Prettyprinter module
+    --   hierarchy was introduced.
+    base          >= 4.11 && < 5,
+    parsec        >= 3.1.17 && < 4.0,
+    prettyprinter >= 1.7 && < 2.0,
+    transformers  >= 0.2 && < 0.7
 
-  ghc-options: -Wall
+  ghc-options: -Wall -Wno-unused-imports -Wno-deriving-typeable
 
 test-suite tests
+  default-language: Haskell2010
   type: exitcode-stdio-1.0
   hs-source-dirs: tests
   main-is: Tests.hs
diff --git a/src/Language/Bash/Cond.hs b/src/Language/Bash/Cond.hs
--- a/src/Language/Bash/Cond.hs
+++ b/src/Language/Bash/Cond.hs
@@ -22,7 +22,7 @@
 import GHC.Generics           (Generic)
 import Text.Parsec            hiding ((<|>), token)
 import Text.Parsec.Expr       hiding (Operator)
-import Data.Text.Prettyprint.Doc (Pretty(..), (<+>))
+import Prettyprinter          (Pretty(..), (<+>))
 
 import Language.Bash.Operator
 
diff --git a/src/Language/Bash/Expand.hs b/src/Language/Bash/Expand.hs
--- a/src/Language/Bash/Expand.hs
+++ b/src/Language/Bash/Expand.hs
@@ -12,11 +12,10 @@
 import Control.Applicative
 import Control.Monad
 import Data.Char
-import Data.Monoid            ((<>))
 import Text.Parsec.Combinator hiding (optional, manyTill)
 import Text.Parsec.Prim       hiding ((<|>), many, token)
 import Text.Parsec.String     ()
-import Data.Text.Prettyprint.Doc (Pretty(..))
+import Prettyprinter          (Pretty(..))
 
 import Language.Bash.Pretty
 import Language.Bash.Word     hiding (prefix)
diff --git a/src/Language/Bash/Operator.hs b/src/Language/Bash/Operator.hs
--- a/src/Language/Bash/Operator.hs
+++ b/src/Language/Bash/Operator.hs
@@ -6,9 +6,9 @@
     , prettyOperator
     ) where
 
-import Control.Applicative
-import Data.Foldable
-import Data.Text.Prettyprint.Doc (Doc, pretty)
+import Control.Applicative (Alternative)
+import Data.Foldable (asum)
+import Prettyprinter (Doc, pretty)
 
 -- | String operators.
 class Eq a => Operator a where
diff --git a/src/Language/Bash/Parse/Builder.hs b/src/Language/Bash/Parse/Builder.hs
--- a/src/Language/Bash/Parse/Builder.hs
+++ b/src/Language/Bash/Parse/Builder.hs
@@ -25,11 +25,11 @@
 
 import           Prelude                hiding (span)
 
-import           Control.Applicative    hiding (many)
-import qualified Control.Applicative    as Applicative
-import           Data.Monoid
+import           Control.Applicative    ((<|>), liftA2)
+import           Data.Monoid            (Endo (..))
+import           Text.Parsec            (ParsecT, Stream)
 import qualified Text.Parsec.Char       as P
-import           Text.Parsec.Prim       hiding ((<|>), many)
+import qualified Text.Parsec.Prim       as P
 
 infixr 4 <+>
 
@@ -53,12 +53,12 @@
 (<+>) = liftA2 mappend
 
 -- | Concat zero or more monoidal results.
-many :: (Alternative f, Monoid a) => f a -> f a
-many = fmap mconcat . Applicative.many
+many :: Monoid a => ParsecT s u m a -> ParsecT s u m a
+many = fmap mconcat . P.many
 
 -- | Concat one or more monoidal results.
-many1 :: (Alternative f, Monoid a) => f a -> f a
-many1 = fmap mconcat . Applicative.some
+many1 :: Monoid a => ParsecT s u m a -> ParsecT s u m a
+many1 = fmap mconcat . P.many1
 
 -- | 'Builder' version of 'P.oneOf'.
 oneOf :: Stream s m Char => [Char] -> ParsecT s u m Builder
diff --git a/src/Language/Bash/Parse/Word.hs b/src/Language/Bash/Parse/Word.hs
--- a/src/Language/Bash/Parse/Word.hs
+++ b/src/Language/Bash/Parse/Word.hs
@@ -48,7 +48,7 @@
 upTo1 :: Alternative f => Int -> f a -> f [a]
 upTo1 n p = (:) <$> p <*> upTo (n - 1) p
 
--- | Parse a span until a delimeter.
+-- | Parse a span until a delimiter.
 spans
     :: Stream s m Char
     => [Char]              -- ^ Delimiters
@@ -365,6 +365,4 @@
 
     continue xs = do
         c <- anyChar
-        (c :) <$> go (prefix c xs)
-
-    prefix c = map tail . filter (\x -> not (null x) && head x == c)
+        (c :) <$> go (mapMaybe (stripPrefix [c]) xs)
diff --git a/src/Language/Bash/Pretty.hs b/src/Language/Bash/Pretty.hs
--- a/src/Language/Bash/Pretty.hs
+++ b/src/Language/Bash/Pretty.hs
@@ -2,9 +2,9 @@
 -- used by the Bash builtin @declare -f@.
 module Language.Bash.Pretty where
 
-import Data.Text.Prettyprint.Doc
-import Data.Text.Prettyprint.Doc.Internal
-import Data.Text.Prettyprint.Doc.Render.String
+import Prettyprinter
+import Prettyprinter.Internal.Type (Doc(Empty))
+import Prettyprinter.Render.String (renderString)
 
 -- | @x $+$ y@ concatenates @x@ and @y@ with a 'line' in between
 ($+$) :: Doc ann -> Doc ann -> Doc ann
diff --git a/src/Language/Bash/Syntax.hs b/src/Language/Bash/Syntax.hs
--- a/src/Language/Bash/Syntax.hs
+++ b/src/Language/Bash/Syntax.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, OverloadedStrings, RecordWildCards, DeriveGeneric #-}
+{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, GeneralizedNewtypeDeriving, OverloadedStrings, RecordWildCards, DeriveGeneric #-}
 -- | Shell script types.
 module Language.Bash.Syntax
     (
@@ -29,11 +29,10 @@
 
 import Data.Data        (Data)
 import Data.List        (intersperse)
-import Data.Semigroup   (Semigroup(..))
 import Data.Typeable    (Typeable)
 import GHC.Generics     (Generic)
-import Data.Text.Prettyprint.Doc (Doc, Pretty(..), (<+>), hardline, hcat, hsep, indent, nest, nesting, punctuate, vcat)
-import Data.Text.Prettyprint.Doc.Internal (Doc(Empty))
+import Prettyprinter    (Doc, Pretty(..), (<+>), hardline, hcat, hsep, indent, nest, nesting, punctuate, vcat)
+import Prettyprinter.Internal.Type (Doc(Empty))
 
 import Language.Bash.Cond     (CondExpr)
 import Language.Bash.Operator
@@ -88,9 +87,14 @@
 prettyHeredocs [] = mempty
 prettyHeredocs rs = mconcat $ intersperse hardline $ map prettyHeredoc rs
     where
-        prettyHeredoc Heredoc{..} = pretty hereDocument <> pretty heredocDelim
+        prettyHeredoc Heredoc{..} = pretty (ensureTrailingNewline hereDocument) <> pretty heredocDelim
         prettyHeredoc _ = mempty
 
+        ensureTrailingNewline [] = []
+        ensureTrailingNewline xs
+            | last xs == Char '\n' = xs
+            | otherwise = xs ++ [Char '\n']
+
 -- | Indent by 4 columns.
 indent' :: Doc ann -> Doc ann
 indent' = indent 4
@@ -125,7 +129,7 @@
 instance ToBashDoc Command where
     toBashDoc (Command c rs) = BashDoc mempty (pretty c <++> pretty rs) (prettyHeredocs $ filter isHeredoc rs)
         where
-            isHeredoc Heredoc{..} = True
+            isHeredoc Heredoc{} = True
             isHeredoc _ = False
 
 -- | A Bash command.
@@ -316,7 +320,7 @@
 
 -- | A compound list of statements.
 newtype List = List [Statement]
-    deriving (Data, Eq, Read, Show, Typeable, Generic)
+    deriving (Data, Eq, Monoid, Read, Semigroup, Show, Typeable, Generic)
 
 instance Pretty List where
     pretty (List as) = pretty as
diff --git a/src/Language/Bash/Word.hs b/src/Language/Bash/Word.hs
--- a/src/Language/Bash/Word.hs
+++ b/src/Language/Bash/Word.hs
@@ -29,11 +29,10 @@
 import Prelude hiding (Word)
 
 import           Data.Data        (Data)
-import           Data.Monoid      ((<>))
 import           Data.Typeable    (Typeable)
 import           GHC.Generics     (Generic)
-import           Data.Text.Prettyprint.Doc (Doc, Pretty(..), hcat, hsep, layoutCompact)
-import           Data.Text.Prettyprint.Doc.Render.String (renderString)
+import           Prettyprinter    (Doc, Pretty(..), hcat, hsep, layoutCompact)
+import           Prettyprinter.Render.String (renderString)
 
 import           Language.Bash.Operator
 
diff --git a/tests/Tests.hs b/tests/Tests.hs
--- a/tests/Tests.hs
+++ b/tests/Tests.hs
@@ -188,6 +188,32 @@
                   heredocDelim = "EOF",
                   heredocDelimQuoted = False,
                   hereDocument = stringToWord "comment\n"}])
+  , testGroup "Issue #23 regression tests"
+    [ testCase "Empty document" $ do
+          let list = wrapCommand $ Command
+                  (SimpleCommand [] [stringToWord "command"])
+                  [ Heredoc
+                      { heredocOp = Here
+                      , heredocDelim = "EOF"
+                      , heredocDelimQuoted = False
+                      , hereDocument = []
+                      }
+                  ]
+              expected = "command <<EOF;\nEOF"
+          expected @=? prettyText list
+    , testCase "Non-empty document" $ do
+          let list = wrapCommand $ Command
+                  (SimpleCommand [] [stringToWord "command"])
+                  [ Heredoc
+                      { heredocOp = Here
+                      , heredocDelim = "EOF"
+                      , heredocDelimQuoted = False
+                      , hereDocument = stringToWord "here document"
+                      }
+                  ]
+              expected = "command <<EOF;\nhere document\nEOF"
+          expected @=? prettyText list
+    ]
   ]
 
 failingtests :: TestTree
diff --git a/tests/pretty/conditionals-simple.out b/tests/pretty/conditionals-simple.out
deleted file mode 100644
--- a/tests/pretty/conditionals-simple.out
+++ /dev/null
@@ -1,35 +0,0 @@
-if true; then
-    true;
-fi;
-if true; then
-    true;
-else
-    if false; then
-        false;
-    fi;
-fi;
-if true; then
-    true;
-else
-    false;
-fi;
-if true; then
-    true;
-else
-    if false; then
-        false;
-    else
-        false;
-    fi;
-fi;
-case x in
-    x)
-        true;
-        ;;
-esac;
-select x; do
-    true;
-done;
-select x in y z; do
-    true;
-done;
diff --git a/tests/pretty/heredoc-composed.out b/tests/pretty/heredoc-composed.out
deleted file mode 100644
--- a/tests/pretty/heredoc-composed.out
+++ /dev/null
@@ -1,30 +0,0 @@
-cat <<EOF1 |
-Here doc 1
-EOF1
-cat <<EOF2;
-Here doc 2
-EOF2
-cat <<EOF1 &&
-Here doc 1
-EOF1
-cat <<EOF2;
-Here doc 2
-EOF2
-cat <<EOF1 ||
-Here doc 1
-EOF1
-cat <<EOF2;
-Here doc 2
-EOF2
-cat <<EOF1 |
-Here doc 1
-EOF1
-for x in true; do
-    true;
-done;
-cat <<EOF1 |
-Here doc 1
-EOF1
-while true; do
-    true;
-done;
diff --git a/tests/pretty/heredoc-in-condition.out b/tests/pretty/heredoc-in-condition.out
deleted file mode 100644
--- a/tests/pretty/heredoc-in-condition.out
+++ /dev/null
@@ -1,18 +0,0 @@
-if true <<EOF;
-here doc
-EOF
-then
-    true;
-fi;
-until true <<EOF;
-here doc
-EOF
-do
-    true;
-done;
-while true <<EOF;
-here doc
-EOF
-do
-    true;
-done;
diff --git a/tests/pretty/heredoc-nested.out b/tests/pretty/heredoc-nested.out
deleted file mode 100644
--- a/tests/pretty/heredoc-nested.out
+++ /dev/null
@@ -1,26 +0,0 @@
-for x in true; do
-    cat <<EOF;
-here doc
-EOF
-done;
-for x in true; do
-    for y in true; do
-        cat <<EOF;
-here doc
-EOF
-        true;
-    done;
-done;
-while true; do
-    cat <<EOF;
-here doc
-EOF
-done;
-while true; do
-    while true; do
-        cat <<EOF;
-here doc
-EOF
-        true;
-    done;
-done;
diff --git a/tests/pretty/heredoc.out b/tests/pretty/heredoc.out
deleted file mode 100644
--- a/tests/pretty/heredoc.out
+++ /dev/null
@@ -1,11 +0,0 @@
-cat <<EOF;
-Hello world!
-EOF
-cat <<EOF1 <<EOF2;
-Here doc 1
-EOF1
-Here doc 2
-EOF2
-cat <<EOF >/dev/null;
-Hello world!
-EOF
diff --git a/tests/pretty/loops-nested.out b/tests/pretty/loops-nested.out
deleted file mode 100644
--- a/tests/pretty/loops-nested.out
+++ /dev/null
@@ -1,15 +0,0 @@
-for x in true; do
-    for y in true; do
-        true;
-    done;
-done;
-until true; do
-    until true; do
-        true;
-    done;
-done;
-while true; do
-    while true; do
-        true;
-    done;
-done;
diff --git a/tests/pretty/loops-simple.out b/tests/pretty/loops-simple.out
deleted file mode 100644
--- a/tests/pretty/loops-simple.out
+++ /dev/null
@@ -1,9 +0,0 @@
-for x in true; do
-    true;
-done;
-until true; do
-    true;
-done;
-while true; do
-    true;
-done;
diff --git a/tests/pretty/simple.out b/tests/pretty/simple.out
deleted file mode 100644
--- a/tests/pretty/simple.out
+++ /dev/null
@@ -1,14 +0,0 @@
-declare foo;
-declare foo=bar;
-declare foo1=bar1 foo2=bar2;
-declare -i foo;
-declare -a foo;
-declare -r foo;
-cmd;
-cmd1 | cmd2;
-time cmd1 | cmd2;
-time -p cmd1 | cmd2;
-! cmd1 | cmd2;
-time -p ! cmd1 | cmd2;
-cmd1 && cmd2;
-cmd1 || cmd2;
