diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,33 +0,0 @@
-Changelog
-
-## [1.1.3](https://github.com/haskell-github-trust/megaparsec-utils/compare/1.1.2...1.1.3) (2025-08-07)
-
-## [1.1.2](https://github.com/haskell-github-trust/megaparsec-utils/compare/1.1.1...1.1.2) (2025-07-29)
-
-## [1.1.1](https://github.com/haskell-github-trust/megaparsec-utils/compare/1.1.0...1.1.1) (2025-07-28)
-
-# [1.1.0](https://github.com/haskell-github-trust/megaparsec-utils/compare/1.0.0...1.1.0) (2025-07-28)
-
-
-### Features
-
-* add on/off to booleans ([1e9b885](https://github.com/haskell-github-trust/megaparsec-utils/commit/1e9b88532ec86ef3bf229de01c0a5a81a89261ec))
-
-# 1.0.0 (2025-07-28)
-
-
-### Bug Fixes
-
-* fix date parser with day short names ([64f3229](https://github.com/haskell-github-trust/megaparsec-utils/commit/64f3229dcfd660bdd770938da0bbacbf84c02dc3))
-* fix invalid generation of TimeOfDay ([6bc728e](https://github.com/haskell-github-trust/megaparsec-utils/commit/6bc728ea31e2cfb380927c53ee544f44b399ece7))
-* fix type errors in boundedEnumShowParser ([9c0fe3d](https://github.com/haskell-github-trust/megaparsec-utils/commit/9c0fe3db132bfe123be2d71e7c92f5928113cc26))
-
-
-### Features
-
-* add aeson parser transformer ([84857eb](https://github.com/haskell-github-trust/megaparsec-utils/commit/84857eb2d5221f8547e9130c2b8503e7e59888a4))
-* add date parser ([357d03f](https://github.com/haskell-github-trust/megaparsec-utils/commit/357d03f265e053f128be8711aec53e0b7659e563))
-* add duration parsers ([a89bd88](https://github.com/haskell-github-trust/megaparsec-utils/commit/a89bd885b867b65cb7db3fcad4635cd38cba2227))
-* add gregorian day parser ([46d4178](https://github.com/haskell-github-trust/megaparsec-utils/commit/46d4178945ad8bff5b309b95d9a2901a53b60ef9))
-* add parser for decimal numbers ([42de75f](https://github.com/haskell-github-trust/megaparsec-utils/commit/42de75fec47b29caf1de59334b7a9c5e05f87a2f))
-* add time parsers ([f267ffd](https://github.com/haskell-github-trust/megaparsec-utils/commit/f267ffd718f701046965318f99aa73b7f9e7b335))
diff --git a/megaparsec-utils.cabal b/megaparsec-utils.cabal
--- a/megaparsec-utils.cabal
+++ b/megaparsec-utils.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.37.0.
+-- This file has been generated from package.yaml by hpack version 0.39.6.
 --
 -- see: https://github.com/sol/hpack
 
 name:           megaparsec-utils
-version:        0.1.1.3
+version:        0.2.0.0
 synopsis:       Parsers and utilities for the Megaparsec library.
 description:    Common parsers and utilities to use with the Megaparsec library.
 category:       Parsing
@@ -18,12 +18,11 @@
 license-file:   LICENSE
 build-type:     Simple
 tested-with:
-    GHC == 9.8
+    GHC == 9.10
+  , GHC == 9.8
   , GHC == 9.6
-  , GHC == 9.4
 extra-source-files:
     README.md
-    CHANGELOG.md
 
 source-repository head
   type: git
@@ -32,6 +31,9 @@
 library
   exposed-modules:
       Text.Megaparsec.Utils
+      Text.Megaparsec.Utils.Byte
+      Text.Megaparsec.Utils.Char
+      Text.Megaparsec.Utils.Common
   other-modules:
       Paths_megaparsec_utils
   hs-source-dirs:
@@ -40,6 +42,7 @@
   build-depends:
       aeson >=2.0 && <3
     , base >=4.7 && <5
+    , bytestring >=0.11 && <0.13
     , megaparsec >=9.0 && <10
     , parser-combinators >=1.0 && <2
     , text >=2.0 && <3
@@ -51,6 +54,7 @@
   main-is: Spec.hs
   other-modules:
       Text.Megaparsec.UtilsSpec
+      Text.Megaparsec.UtilsStreamSpec
       Paths_megaparsec_utils
   hs-source-dirs:
       test
@@ -59,6 +63,7 @@
       QuickCheck
     , aeson >=2.0 && <3
     , base >=4.7 && <5
+    , bytestring >=0.11 && <0.13
     , hspec
     , megaparsec >=9.0 && <10
     , megaparsec-utils
diff --git a/src/Text/Megaparsec/Utils.hs b/src/Text/Megaparsec/Utils.hs
--- a/src/Text/Megaparsec/Utils.hs
+++ b/src/Text/Megaparsec/Utils.hs
@@ -1,7 +1,3 @@
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeFamilies #-}
-
 -- |
 -- Module      : Text.Megaparsec.Utils
 -- Description : Various generic parsers and combinators.
@@ -10,133 +6,38 @@
 -- Maintainer  : drlkf@drlkf.net
 -- Stability   : experimental
 --
--- Generic utilities and common parsers.
+-- Re-exports of 'Text.Megaparsec.Utils.Char' and 'Text.Megaparsec.Utils.Common',
+-- plus 'String'-based conversion helpers. New code should use
+-- 'Text.Megaparsec.Utils.Char' or 'Text.Megaparsec.Utils.Byte' directly.
 module Text.Megaparsec.Utils (
   -- * Scalar parsers
-  boolParser,
-  numParser,
-  posDecNumParser,
-  posNumParser,
-  uuidParser,
+  Char.boolParser,
+  Char.numParser,
+  Char.posDecNumParser,
+  Char.posNumParser,
+  Char.uuidParser,
 
   -- * Combinators
-  commaSeparated,
-  occurrence,
-  occurrences,
+  Char.commaSeparated,
+  Common.occurrence,
+  Common.occurrences,
 
   -- * Compatibility utilities
-  boundedEnumShowParser,
+  Char.boundedEnumShowParser,
   parsecToReadsPrec,
   parsecToJSONParser,
 ) where
 
-import Control.Applicative (many, some, (<|>))
-import Control.Applicative.Combinators (choice)
-import Control.Monad (replicateM)
-import Control.Monad.Combinators (optional)
 import Data.Aeson.Types (Parser, Value, withText)
-import Data.Functor (($>))
-import Data.List (intercalate, sortOn)
-import Data.List.NonEmpty (NonEmpty ((:|)))
-import Data.Maybe (fromJust)
 import qualified Data.Text as T (unpack)
-import Data.UUID (UUID)
-import qualified Data.UUID as U (fromString)
 import Text.Megaparsec (
   Parsec,
   ShowErrorComponent,
-  anySingle,
   errorBundlePretty,
   runParser,
-  try,
  )
-import Text.Megaparsec.Char (
-  char,
-  digitChar,
-  hexDigitChar,
-  string',
- )
-import Text.Read (readMaybe)
-
--- | Parse a case-insensitive human-readable boolean, including C-style numbers,
--- English yes-no and @on@ / @off@.
-boolParser
-  :: Ord e
-  => Parsec e String Bool
-boolParser = true <|> false
- where
-  true = True <$ choice (map string' ["true", "y", "yes", "on", "1"])
-  false = False <$ choice (map string' ["false", "n", "no", "off", "0"])
-
--- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all
--- possibilities, case-insensitive, in the 'Enum' order.
-boundedEnumShowParser
-  :: forall a e
-   . Ord e
-  => Bounded a
-  => Enum a
-  => Show a
-  => Parsec e String a
-boundedEnumShowParser =
-  choice . map parseShow $ sortOn (negate . length . show) [(minBound :: a) ..]
- where
-  parseShow a = string' (show a) $> a
-
--- | Parse a comma-separated list of items.
-commaSeparated
-  :: Ord e
-  => Parsec e String a
-  -> Parsec e String (NonEmpty a)
-commaSeparated p = (:|) <$> p <*> many (char ',' >> p)
-
--- | Parse any occurrence of a given parser. Consumes any input before occurrence.
-occurrence
-  :: Ord e
-  => Parsec e String a
-  -> Parsec e String a
-occurrence p = go
- where
-  go = p <|> (anySingle >> go)
-
--- | Parse all occurrences of a given parser.
-occurrences
-  :: Ord e
-  => Parsec e String a
-  -> Parsec e String [a]
-occurrences = some . try . occurrence . try
-
--- | Parse a positive number, with or without decimals prefixed by a @.@.
-posDecNumParser
-  :: Ord e
-  => Read a
-  => Parsec e String a
-posDecNumParser = do
-  num <- some digitChar
-  dec <- maybe "" ("." <>) <$> optional (char '.' >> some digitChar)
-
-  let str = num <> dec
-
-  maybe (fail ("could not read from input: " <> str)) pure (readMaybe str)
-
--- | Parse a positive integer.
-posNumParser
-  :: Ord e
-  => Read a
-  => Parsec e String a
-posNumParser = do
-  digits <- some digitChar
-  maybe
-    (fail ("could not read from digits: " <> digits))
-    pure
-    (readMaybe digits) 
-
--- | Parse an integer, without any space between minus sign and digits.
-numParser
-  :: Ord e
-  => Num a
-  => Read a
-  => Parsec e String a
-numParser = (char '-' >> negate <$> posNumParser) <|> posNumParser
+import qualified Text.Megaparsec.Utils.Char as Char
+import qualified Text.Megaparsec.Utils.Common as Common
 
 -- | Convert a 'Parsec' parser into a 'Parser' suited for 'Data.Aeson.FromJSON'
 -- instances.
@@ -156,19 +57,3 @@
   :: Parsec e String a
   -> ReadS a
 parsecToReadsPrec p = either (const []) (\x -> [(x, "")]) . runParser p "string"
-
--- | Parse a RFC4122-compliant UUID.
-uuidParser
-  :: Ord e
-  => Parsec e String UUID
-uuidParser = do
-  part1 <- replicateM 8 hexDigitChar <* char '-'
-  part2 <- replicateM 4 hexDigitChar <* char '-'
-  part3 <- replicateM 4 hexDigitChar <* char '-'
-  part4 <- replicateM 4 hexDigitChar <* char '-'
-  part5 <- replicateM 12 hexDigitChar
-
-  pure
-    (fromJust
-     (U.fromString
-      (intercalate "-" [part1, part2, part3, part4, part5])))
diff --git a/src/Text/Megaparsec/Utils/Byte.hs b/src/Text/Megaparsec/Utils/Byte.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Megaparsec/Utils/Byte.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+
+-- |
+-- Module      : Text.Megaparsec.Utils.Byte
+-- Description : Parsers for streams of 'Word8'.
+-- Copyright   : (c) drlkf, 2026
+-- License     : GPL-3
+-- Maintainer  : drlkf@drlkf.net
+-- Stability   : experimental
+--
+-- Generic parsers for streams of 'Word8' (strict and lazy 'ByteString').
+module Text.Megaparsec.Utils.Byte (
+  -- * Scalar parsers
+  boolParser,
+  numParser,
+  posDecNumParser,
+  posNumParser,
+  uuidParser,
+
+  -- * Combinators
+  commaSeparated,
+
+  -- * Compatibility utilities
+  boundedEnumShowParser,
+) where
+
+import Data.List.NonEmpty (NonEmpty)
+import Data.UUID (UUID)
+import Data.Word (Word8)
+import Text.Megaparsec (MonadParsec, Token)
+import Text.Megaparsec.Byte (char, char', digitChar, hexDigitChar)
+import Text.Megaparsec.Utils.Common (
+  mkBoolParser,
+  mkBoundedEnumShowParser,
+  mkCommaSeparated,
+  mkNumParser,
+  mkPosDecNumParser,
+  mkPosNumParser,
+  mkUuidParser,
+ )
+
+-- | Parse a case-insensitive human-readable boolean, including C-style numbers,
+-- English yes-no and @on@ / @off@.
+--
+-- @since 0.2.0.0
+boolParser
+  :: MonadParsec e s m
+  => Token s ~ Word8
+  => m Bool
+boolParser = mkBoolParser char' (toEnum . fromEnum)
+
+-- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all
+-- possibilities, case-insensitive, in the 'Enum' order.
+--
+-- @since 0.2.0.0
+boundedEnumShowParser
+  :: forall a e s m
+   . MonadParsec e s m
+  => Token s ~ Word8
+  => Bounded a
+  => Enum a
+  => Show a
+  => m a
+boundedEnumShowParser = mkBoundedEnumShowParser char' (toEnum . fromEnum)
+
+-- | Parse a comma-separated list of items.
+--
+-- @since 0.2.0.0
+commaSeparated
+  :: MonadParsec e s m
+  => Token s ~ Word8
+  => m a
+  -> m (NonEmpty a)
+commaSeparated = mkCommaSeparated (char 44)
+
+-- | Parse a positive number, with or without decimals prefixed by a @.@.
+--
+-- @since 0.2.0.0
+posDecNumParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Read a
+  => Token s ~ Word8
+  => m a
+posDecNumParser = mkPosDecNumParser digitChar (char 46) (toEnum . fromIntegral)
+
+-- | Parse a positive integer.
+--
+-- @since 0.2.0.0
+posNumParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Read a
+  => Token s ~ Word8
+  => m a
+posNumParser = mkPosNumParser digitChar (toEnum . fromIntegral)
+
+-- | Parse an integer, without any space between minus sign and digits.
+--
+-- @since 0.2.0.0
+numParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Num a
+  => Read a
+  => Token s ~ Word8
+  => m a
+numParser = mkNumParser (char 45) posNumParser
+
+-- | Parse a RFC4122-compliant UUID.
+--
+-- @since 0.2.0.0
+uuidParser
+  :: MonadParsec e s m
+  => Token s ~ Word8
+  => m UUID
+uuidParser = mkUuidParser hexDigitChar (char 45) (toEnum . fromIntegral)
diff --git a/src/Text/Megaparsec/Utils/Char.hs b/src/Text/Megaparsec/Utils/Char.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Megaparsec/Utils/Char.hs
@@ -0,0 +1,143 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+
+-- |
+-- Module      : Text.Megaparsec.Utils.Char
+-- Description : Parsers for streams of 'Char'.
+-- Copyright   : (c) drlkf, 2024
+-- License     : GPL-3
+-- Maintainer  : drlkf@drlkf.net
+-- Stability   : experimental
+--
+-- Generic parsers for streams of 'Char' ('String', 'Text', lazy 'Text').
+module Text.Megaparsec.Utils.Char (
+  -- * Scalar parsers
+  boolParser,
+  numParser,
+  posDecNumParser,
+  posNumParser,
+  uuidParser,
+
+  -- * Combinators
+  commaSeparated,
+
+  -- * Compatibility utilities
+  boundedEnumShowParser,
+  parsecToJSONParser,
+) where
+
+import Data.Aeson.Types (Parser, Value, withText)
+import Data.List.NonEmpty (NonEmpty)
+import Data.Text (Text)
+import Data.UUID (UUID)
+import Text.Megaparsec (
+  MonadParsec,
+  Parsec,
+  ShowErrorComponent,
+  Token,
+  errorBundlePretty,
+  runParser,
+ )
+import Text.Megaparsec.Char (char, char', digitChar, hexDigitChar)
+import Text.Megaparsec.Utils.Common (
+  mkBoolParser,
+  mkBoundedEnumShowParser,
+  mkCommaSeparated,
+  mkNumParser,
+  mkPosDecNumParser,
+  mkPosNumParser,
+  mkUuidParser,
+ )
+
+-- | Parse a case-insensitive human-readable boolean, including C-style numbers,
+-- English yes-no and @on@ / @off@.
+--
+-- @since 0.2.0.0
+boolParser
+  :: MonadParsec e s m
+  => Token s ~ Char
+  => m Bool
+boolParser = mkBoolParser char' id
+
+-- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all
+-- possibilities, case-insensitive, in the 'Enum' order.
+--
+-- @since 0.2.0.0
+boundedEnumShowParser
+  :: forall a e s m
+   . MonadParsec e s m
+  => Token s ~ Char
+  => Bounded a
+  => Enum a
+  => Show a
+  => m a
+boundedEnumShowParser = mkBoundedEnumShowParser char' id
+
+-- | Parse a comma-separated list of items.
+--
+-- @since 0.2.0.0
+commaSeparated
+  :: MonadParsec e s m
+  => Token s ~ Char
+  => m a
+  -> m (NonEmpty a)
+commaSeparated = mkCommaSeparated (char ',')
+
+-- | Parse a positive number, with or without decimals prefixed by a @.@.
+--
+-- @since 0.2.0.0
+posDecNumParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Read a
+  => Token s ~ Char
+  => m a
+posDecNumParser = mkPosDecNumParser digitChar (char '.') id
+
+-- | Parse a positive integer.
+--
+-- @since 0.2.0.0
+posNumParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Read a
+  => Token s ~ Char
+  => m a
+posNumParser = mkPosNumParser digitChar id
+
+-- | Parse an integer, without any space between minus sign and digits.
+--
+-- @since 0.2.0.0
+numParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Num a
+  => Read a
+  => Token s ~ Char
+  => m a
+numParser = mkNumParser (char '-') posNumParser
+
+-- | Convert a 'Parsec' parser on 'Text' into a 'Parser' suited for
+-- 'Data.Aeson.FromJSON' instances.
+--
+-- @since 0.2.0.0
+parsecToJSONParser
+  :: ShowErrorComponent e
+  => String
+  -- ^ Parser name.
+  -> Parsec e Text a
+  -- ^ Parser.
+  -> (Value -> Parser a)
+parsecToJSONParser n p =
+  withText n (either (fail . errorBundlePretty) pure . runParser p n)
+
+-- | Parse a RFC4122-compliant UUID.
+--
+-- @since 0.2.0.0
+uuidParser
+  :: MonadParsec e s m
+  => Token s ~ Char
+  => m UUID
+uuidParser = mkUuidParser hexDigitChar (char '-') id
diff --git a/src/Text/Megaparsec/Utils/Common.hs b/src/Text/Megaparsec/Utils/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Megaparsec/Utils/Common.hs
@@ -0,0 +1,188 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+-- Module      : Text.Megaparsec.Utils.Common
+-- Description : Stream-agnostic combinators.
+-- Copyright   : (c) drlkf, 2024
+-- License     : GPL-3
+-- Maintainer  : drlkf@drlkf.net
+-- Stability   : experimental
+--
+-- Combinators that work on any stream.
+module Text.Megaparsec.Utils.Common (
+  occurrence,
+  occurrences,
+
+  -- * Shared parser builders
+  mkBoolParser,
+  mkBoundedEnumShowParser,
+  mkCommaSeparated,
+  mkNumParser,
+  mkPosDecNumParser,
+  mkPosNumParser,
+  mkUuidParser,
+) where
+
+import Control.Applicative (many, some, (<|>))
+import Control.Applicative.Combinators (choice)
+import Control.Monad (replicateM)
+import Control.Monad.Combinators (optional)
+import Data.Foldable (traverse_)
+import Data.Functor (($>))
+import Data.List (intercalate, sortOn)
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Maybe (fromJust)
+import Data.UUID (UUID)
+import qualified Data.UUID as U (fromString)
+import Text.Megaparsec (MonadParsec, Token, anySingle, try)
+import Text.Read (readMaybe)
+
+-- | Parse any occurrence of a given parser. Consumes any input before occurrence.
+--
+-- @since 0.2.0.0
+occurrence :: MonadParsec e s m => m a -> m a
+occurrence p = go
+ where
+  go = p <|> (anySingle >> go)
+
+-- | Parse all occurrences of a given parser.
+--
+-- @since 0.2.0.0
+occurrences :: MonadParsec e s m => m a -> m [a]
+occurrences = some . try . occurrence . try
+
+-- | Build a case-insensitive human-readable boolean parser, including C-style
+-- numbers, English yes-no and @on@ / @off@.
+--
+-- @since 0.2.0.0
+mkBoolParser
+  :: MonadParsec e s m
+  => (Token s -> m (Token s))
+  -- ^ Case-insensitive single-token parser.
+  -> (Char -> Token s)
+  -- ^ Convert a literal 'Char' to a token.
+  -> m Bool
+mkBoolParser char' fromChar = true <|> false
+ where
+  true = True <$ choice (map (try . traverse_ (char' . fromChar)) ["true", "y", "yes", "on", "1"])
+  false = False <$ choice (map (try . traverse_ (char' . fromChar)) ["false", "n", "no", "off", "0"])
+
+-- | Build a parser for a 'Bounded' 'Enum' type that has a 'Show' instance,
+-- trying all possibilities, case-insensitive, in the 'Enum' order.
+--
+-- @since 0.2.0.0
+mkBoundedEnumShowParser
+  :: forall a e s m
+   . MonadParsec e s m
+  => Bounded a
+  => Enum a
+  => Show a
+  => (Token s -> m (Token s))
+  -- ^ Case-insensitive single-token parser.
+  -> (Char -> Token s)
+  -- ^ Convert a literal 'Char' to a token.
+  -> m a
+mkBoundedEnumShowParser char' fromChar =
+  choice . map parseShow $ sortOn (negate . length . show) [(minBound :: a) ..]
+ where
+  parseShow a = try (traverse_ (char' . fromChar) (show a)) $> a
+
+-- | Build a parser for a comma-separated list of items.
+--
+-- @since 0.2.0.0
+mkCommaSeparated
+  :: MonadParsec e s m
+  => m (Token s)
+  -- ^ Comma parser.
+  -> m a
+  -> m (NonEmpty a)
+mkCommaSeparated comma p = (:|) <$> p <*> many (comma >> p)
+
+-- | Build a parser for a positive number, with or without decimals prefixed by
+-- a @.@.
+--
+-- @since 0.2.0.0
+mkPosDecNumParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Read a
+  => m (Token s)
+  -- ^ Digit parser.
+  -> m (Token s)
+  -- ^ Decimal-point parser.
+  -> (Token s -> Char)
+  -- ^ Convert a token to a 'Char'.
+  -> m a
+mkPosDecNumParser digitChar dot toChar = do
+  num <- some digitChar
+  dec <- optional (dot >> some digitChar)
+
+  let toStr = map toChar
+      str = toStr num <> maybe "" (('.' :) . toStr) dec
+
+  maybe (fail ("could not read from input: " <> str)) pure (readMaybe str)
+
+-- | Build a parser for a positive integer.
+--
+-- @since 0.2.0.0
+mkPosNumParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Read a
+  => m (Token s)
+  -- ^ Digit parser.
+  -> (Token s -> Char)
+  -- ^ Convert a token to a 'Char'.
+  -> m a
+mkPosNumParser digitChar toChar = do
+  digits <- some digitChar
+  let str = map toChar digits
+  maybe
+    (fail ("could not read from digits: " <> str))
+    pure
+    (readMaybe str)
+
+-- | Build a parser for an integer, without any space between minus sign and
+-- digits.
+--
+-- @since 0.2.0.0
+mkNumParser
+  :: MonadFail m
+  => MonadParsec e s m
+  => Num a
+  => m (Token s)
+  -- ^ Minus-sign parser.
+  -> m a
+  -- ^ Positive-number parser.
+  -> m a
+mkNumParser minus posNumParser = (minus >> negate <$> posNumParser) <|> posNumParser
+
+-- | Build a parser for a RFC4122-compliant UUID.
+--
+-- @since 0.2.0.0
+mkUuidParser
+  :: MonadParsec e s m
+  => m (Token s)
+  -- ^ Hex-digit parser.
+  -> m (Token s)
+  -- ^ Dash parser.
+  -> (Token s -> Char)
+  -- ^ Convert a token to a 'Char'.
+  -> m UUID
+mkUuidParser hexDigitChar dash toChar = do
+  part1 <- replicateM 8 hexDigitChar <* dash
+  part2 <- replicateM 4 hexDigitChar <* dash
+  part3 <- replicateM 4 hexDigitChar <* dash
+  part4 <- replicateM 4 hexDigitChar <* dash
+  part5 <- replicateM 12 hexDigitChar
+
+  let toStr = map toChar
+
+  pure
+    ( fromJust
+        ( U.fromString
+            (intercalate "-" (map toStr [part1, part2, part3, part4, part5]))
+        )
+    )
diff --git a/test/Text/Megaparsec/UtilsSpec.hs b/test/Text/Megaparsec/UtilsSpec.hs
--- a/test/Text/Megaparsec/UtilsSpec.hs
+++ b/test/Text/Megaparsec/UtilsSpec.hs
@@ -158,8 +158,8 @@
 
     context "posDecNumParser" $ do
       it "no decimals" . property $ \v ->
-        parseMaybe' posDecNumParser (show (abs (v :: Int)))
-          `shouldBe` Just (fromIntegral (abs v))
+        parseMaybe' (posDecNumParser :: Parsec Void String Int) (show (abs (v :: Int)))
+          `shouldBe` Just (abs v)
 
       it "decimals" . property $ \v ->
         parseMaybe' posDecNumParser (printf "%f" (abs (v :: Double)))
@@ -235,17 +235,17 @@
 
     context "invalid" $ do
       it "empty" $
-        parseOrPrettyError (commaSeparated (numParser @Void @Int)) ""
+        parseOrPrettyError (commaSeparated (numParser :: Parsec Void String Int)) ""
         `shouldSatisfy` isLeft
 
       it "first" $
-        parseOrPrettyError (commaSeparated (numParser @Void @Int)) "test"
+        parseOrPrettyError (commaSeparated (numParser :: Parsec Void String Int)) "test"
         `shouldSatisfy` isLeft
 
       it "first partially correct" $
-        parseOrPrettyError (commaSeparated ((numParser @Void @Int) <* eof)) "test"
+        parseOrPrettyError (commaSeparated ((numParser :: Parsec Void String Int) <* eof)) "test"
         `shouldSatisfy` isLeft
 
       it "second" $
-        parseOrPrettyError (commaSeparated (numParser @Void @Int)) "test"
+        parseOrPrettyError (commaSeparated (numParser :: Parsec Void String Int)) "test"
         `shouldSatisfy` isLeft
diff --git a/test/Text/Megaparsec/UtilsStreamSpec.hs b/test/Text/Megaparsec/UtilsStreamSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Megaparsec/UtilsStreamSpec.hs
@@ -0,0 +1,66 @@
+{-# LANGUAGE TypeApplications #-}
+
+module Text.Megaparsec.UtilsStreamSpec (
+  spec,
+) where
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Char8 as B
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Void (Void)
+import Test.Hspec (Spec, describe, it, shouldBe)
+import Test.QuickCheck (property)
+import Text.Megaparsec (ParseErrorBundle, Parsec, runParser)
+import qualified Text.Megaparsec.Utils.Byte as Byte
+import qualified Text.Megaparsec.Utils.Char as Char
+import Text.Megaparsec.Utils.Common (occurrence)
+
+parseText :: Parsec Void Text a -> Text -> Either (ParseErrorBundle Text Void) a
+parseText p = runParser p "test"
+
+parseBytes
+  :: Parsec Void ByteString a
+  -> ByteString
+  -> Either (ParseErrorBundle ByteString Void) a
+parseBytes p = runParser p "test"
+
+charNumParser :: Parsec Void Text Int
+charNumParser = Char.numParser
+
+byteNumParser :: Parsec Void ByteString Int
+byteNumParser = Byte.numParser
+
+spec :: Spec
+spec = do
+  describe "Char parsers on Text" $ do
+    it "numParser" . property $ \v ->
+      parseText charNumParser (T.pack (show (v :: Int))) `shouldBe` Right v
+
+    it "boolParser" $ do
+      parseText Char.boolParser (T.pack "yes") `shouldBe` Right True
+      parseText Char.boolParser (T.pack "OFF") `shouldBe` Right False
+
+    it "uuidParser" $ do
+      parseText Char.uuidParser (T.pack "123e4567-e89b-12d3-a456-426614174000")
+        `shouldBe` Right (read "123e4567-e89b-12d3-a456-426614174000")
+
+    it "occurrence" . property $ \v ->
+      parseText (occurrence charNumParser) (T.pack ("abc " <> show (v :: Int)))
+        `shouldBe` Right v
+
+  describe "Byte parsers on ByteString" $ do
+    it "numParser" . property $ \v ->
+      parseBytes byteNumParser (B.pack (show (v :: Int))) `shouldBe` Right v
+
+    it "boolParser" $ do
+      parseBytes Byte.boolParser (B.pack "yes") `shouldBe` Right True
+      parseBytes Byte.boolParser (B.pack "OFF") `shouldBe` Right False
+
+    it "uuidParser" $ do
+      parseBytes Byte.uuidParser (B.pack "123e4567-e89b-12d3-a456-426614174000")
+        `shouldBe` Right (read "123e4567-e89b-12d3-a456-426614174000")
+
+    it "occurrence" . property $ \v ->
+      parseBytes (occurrence byteNumParser) (B.pack ("abc " <> show (v :: Int)))
+        `shouldBe` Right v
