diff --git a/ginger.cabal b/ginger.cabal
--- a/ginger.cabal
+++ b/ginger.cabal
@@ -2,7 +2,7 @@
 -- documentation, see http://haskell.org/cabal/users-guide/
 
 name:                ginger
-version:             0.8.4.1
+version:             0.9.0.0
 synopsis:            An implementation of the Jinja2 template language in Haskell
 description:         Ginger is Jinja, minus the most blatant pythonisms. Wants
                      to be feature complete, but isn't quite there yet.
diff --git a/src/Text/Ginger/GVal.hs b/src/Text/Ginger/GVal.hs
--- a/src/Text/Ginger/GVal.hs
+++ b/src/Text/Ginger/GVal.hs
@@ -78,6 +78,11 @@
                  , TimeZone (..)
                  , TimeLocale (..)
                  )
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LBS
+import Data.Text.Encoding (encodeUtf8, decodeUtf8)
+import qualified Data.Text.Lazy.Encoding as LText
 
 import Text.Ginger.Html
 
@@ -106,6 +111,7 @@
         , asBoolean :: Bool -- ^ Get value's truthiness
         , asNumber :: Maybe Scientific -- ^ Convert value to a number, if possible
         , asFunction :: Maybe (Function m) -- ^ Access value as a callable function, if it is one
+        , asBytes :: Maybe ByteString -- ^ Access as raw bytes
         , length :: Maybe Int -- ^ Get length of value, if it is a collection (list/dict)
         , isNull :: Bool -- ^ Check if the value is null
         , asJSON :: Maybe JSON.Value -- ^ Provide a custom JSON representation of the value
@@ -122,6 +128,7 @@
         return $ \k -> lookupA k <|> lookupB k
     , asHtml = asHtml a <> asHtml b
     , asText = asText a <> asText b
+    , asBytes = asBytes a <> asBytes b
     , asBoolean = (asBoolean a || asBoolean b) && not (isNull a || isNull b)
     , asNumber = readMay . Text.unpack $ (asText a <> asText b)
     , asFunction = Nothing
@@ -148,6 +155,7 @@
         , asLookup = fmap (fmap marshalGVal .) (asLookup g)
         , asHtml = asHtml g
         , asText = asText g
+        , asBytes = asBytes g
         , asBoolean = asBoolean g
         , asNumber = asNumber g
         , asFunction = Nothing
@@ -173,6 +181,7 @@
         , asLookup = fmap (fmap (marshalGValEx hoist unhoist) .) (asLookup g)
         , asHtml = asHtml g
         , asText = asText g
+        , asBytes = asBytes g
         , asBoolean = asBoolean g
         , asNumber = asNumber g
         , asFunction = marshalFunction hoist unhoist <$> asFunction g
@@ -202,6 +211,7 @@
             , asLookup = Nothing
             , asHtml = unsafeRawHtml ""
             , asText = ""
+            , asBytes = Nothing
             , asBoolean = False
             , asNumber = Nothing
             , asFunction = Nothing
@@ -323,6 +333,7 @@
                 def
                     { asHtml = mconcat . Prelude.map asHtml $ xs
                     , asText = mconcat . Prelude.map asText $ xs
+                    , asBytes = mconcat . Prelude.map asBytes $ xs
                     , asBoolean = not . List.null $ xs
                     , isNull = False
                     , asList = Just $ Prelude.map toGVal xs
@@ -338,6 +349,7 @@
                 def
                     { asHtml = mconcat . Prelude.map asHtml . HashMap.elems $ xs
                     , asText = mconcat . Prelude.map asText . HashMap.elems $ xs
+                    , asBytes = mconcat . Prelude.map asBytes . HashMap.elems $ xs
                     , asBoolean = not . HashMap.null $ xs
                     , isNull = False
                     , asLookup = Just (`HashMap.lookup` xs)
@@ -359,6 +371,7 @@
         def
             { asHtml = html . Text.pack . show $ x
             , asText = Text.pack . show $ x
+            , asBytes = Just . encodeUtf8 . Text.pack . show $ x
             , asBoolean = x /= 0
             , asNumber = Just . fromIntegral $ x
             , isNull = False
@@ -369,6 +382,7 @@
         def
             { asHtml = html $ scientificToText x
             , asText = scientificToText x
+            , asBytes = Just . encodeUtf8 . scientificToText $ x
             , asBoolean = x /= 0
             , asNumber = Just x
             , isNull = False
@@ -382,6 +396,7 @@
         in (orderedDict dayDict)
             { asHtml = html $ formatted
             , asText = formatted
+            , asBytes = Just . encodeUtf8 $ formatted
             , asBoolean = True
             , asNumber = Just . fromIntegral $ julian
             , asList = Just (List.map snd dayDict)
@@ -402,6 +417,7 @@
         in (orderedDict timeDict)
             { asHtml = html $ formatted
             , asText = formatted
+            , asBytes = Just . encodeUtf8 $ formatted
             , asBoolean = True
             , asNumber = Nothing
             , asList = Just (List.map snd timeDict)
@@ -425,6 +441,7 @@
                 ])
             { asHtml = html $ formatted
             , asText = formatted
+            , asBytes = Just . encodeUtf8 $ formatted
             , asBoolean = True
             , asNumber = Nothing
             , asList = Just (List.map snd dtDict)
@@ -455,6 +472,7 @@
         in (dict timeLocaleDict)
             { asHtml = html $ formattedExample
             , asText = formattedExample
+            , asBytes = Just . encodeUtf8 $ formattedExample
             , asBoolean = True
             , asNumber = Nothing
             }
@@ -481,6 +499,7 @@
         in (dict dtDict)
             { asHtml = html $ formatted
             , asText = formatted
+            , asBytes = Just . encodeUtf8 $ formatted
             , asBoolean = True
             , asNumber = Nothing
             }
@@ -513,6 +532,7 @@
             { asHtml = if x then html "1" else html ""
             , asText = if x then "1" else ""
             , asBoolean = x
+            , asBytes = Just $ if x then "1" else "0"
             , asNumber = Just $ if x then 1 else 0
             , isNull = False
             , asJSON = Just (JSON.Bool x)
@@ -527,6 +547,7 @@
         def
             { asHtml = html . Text.pack $ x
             , asText = Text.pack x
+            , asBytes = Just . encodeUtf8 . Text.pack $ x
             , asBoolean = not $ Prelude.null x
             , asNumber = readMay x
             , isNull = False
@@ -542,6 +563,7 @@
         def
             { asHtml = html x
             , asText = x
+            , asBytes = Just . encodeUtf8 $ x
             , asBoolean = not $ Text.null x
             , asNumber = readMay . Text.unpack $ x
             , isNull = False
@@ -552,11 +574,34 @@
         def
             { asHtml = html (LText.toStrict x)
             , asText = LText.toStrict x
+            , asBytes = Just . LBS.toStrict . LText.encodeUtf8 $ x
             , asBoolean = not $ LText.null x
             , asNumber = readMay . LText.unpack $ x
             , isNull = False
             }
 
+instance ToGVal m ByteString where
+    toGVal x =
+        def
+            { asHtml = html (decodeUtf8 x)
+            , asText = decodeUtf8 x
+            , asBytes = Just x
+            , asBoolean = not $ BS.null x
+            , asNumber = readMay . Text.unpack . decodeUtf8 $ x
+            , isNull = False
+            }
+
+instance ToGVal m LBS.ByteString where
+    toGVal x =
+        def
+            { asHtml = html . LText.toStrict . LText.decodeUtf8 $ x
+            , asText = LText.toStrict . LText.decodeUtf8 $ x
+            , asBytes = Just . LBS.toStrict $ x
+            , asBoolean = not $ LBS.null x
+            , asNumber = readMay . LText.unpack . LText.decodeUtf8 $ x
+            , isNull = False
+            }
+--
 -- | This instance is slightly wrong; the 'asBoolean', 'asNumber', and 'asText'
 -- methods all treat the HTML source as plain text. We do this to avoid parsing
 -- the HTML back into a 'Text' (and dealing with possible parser errors); the
@@ -794,6 +839,12 @@
 
 instance FromGVal m (GVal m) where
     fromGVal = Just
+
+instance FromGVal m ByteString where
+    fromGVal = asBytes
+
+instance FromGVal m LBS.ByteString where
+    fromGVal = fmap LBS.fromStrict . asBytes
 
 instance FromGVal m a => FromGVal m (Maybe a) where
     fromGVal = \g ->
diff --git a/src/Text/Ginger/Run.hs b/src/Text/Ginger/Run.hs
--- a/src/Text/Ginger/Run.hs
+++ b/src/Text/Ginger/Run.hs
@@ -187,6 +187,7 @@
     , ("show", fromFunction . unaryFunc $ fromString . show)
     , ("slice", fromFunction gfnSlice)
     , ("sort", fromFunction gfnSort)
+    , ("split", fromFunction gfnSplit)
     , ("str", fromFunction . unaryFunc $ toGVal . asText)
     , ("sum", fromFunction . variadicNumericFunc 0 $ Prelude.sum)
     , ("truncate", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.truncate)
diff --git a/src/Text/Ginger/Run/Builtins.hs b/src/Text/Ginger/Run/Builtins.hs
--- a/src/Text/Ginger/Run/Builtins.hs
+++ b/src/Text/Ginger/Run/Builtins.hs
@@ -339,6 +339,23 @@
             return . toGVal $ Text.replace search replace str
         _ -> throwHere $ ArgumentsError (Just "replace") "expected: (str, search, replace)"
 
+gfnSplit :: Monad m => Function (Run p m h)
+gfnSplit args =
+    let argValues =
+            extractArgsDefL
+                [ ("str", def)
+                , ("sep", def)
+                ]
+                args
+    in case argValues of
+        Right [strG, sepG] -> do
+            let search = asText strG
+                split = case asText sepG of
+                    "" -> Text.words
+                    sep -> Text.splitOn sep
+            return . toGVal . split $ search
+        _ -> throwHere $ ArgumentsError (Just "split") "expected: (str, sep=null)"
+
 gfnCompose :: forall m p h. Monad m => Function (Run p m h)
 gfnCompose [] = fail "Invalid arguments to compose()"
 gfnCompose [(_, fG)] = return fG
@@ -855,3 +872,5 @@
     )
     RE.blankCompOpt
     str
+
+-- vim: sw=4 ts=4 expandtab
diff --git a/test/Text/Ginger/PropertyTests.hs b/test/Text/Ginger/PropertyTests.hs
--- a/test/Text/Ginger/PropertyTests.hs
+++ b/test/Text/Ginger/PropertyTests.hs
@@ -8,6 +8,9 @@
 import Test.Tasty.QuickCheck
 import Data.Text (Text)
 import qualified Data.Text as Text
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LBS
 import Data.Default (def)
 import qualified Data.HashMap.Strict as HashMap
 import Control.Exception
@@ -21,6 +24,12 @@
 instance Arbitrary Text where
     arbitrary = Text.pack <$> arbitrary
 
+instance Arbitrary ByteString where
+    arbitrary = BS.pack <$> arbitrary
+
+instance Arbitrary LBS.ByteString where
+    arbitrary = LBS.pack <$> arbitrary
+
 instance Arbitrary Html where
     arbitrary = oneof
         [ html <$> arbitrary
@@ -100,6 +109,7 @@
         , testProperty "Bool" (roundTripGValP :: Bool -> Bool)
         , testProperty "[Text]" (roundTripGValP :: [Text] -> Bool)
         , testProperty "Maybe Text" (roundTripGValP :: Maybe Text -> Bool)
+        , testProperty "ByteString" (roundTripGValP :: Maybe ByteString -> Bool)
         , testProperty "Text" (roundTripGValP :: Text -> Bool)
         , testProperty "LocalTime" (roundTripGValP :: LocalTime -> Bool)
         , testProperty "TimeZone" (roundTripGValP :: TimeZone -> Bool)
diff --git a/test/Text/Ginger/SimulationTests.hs b/test/Text/Ginger/SimulationTests.hs
--- a/test/Text/Ginger/SimulationTests.hs
+++ b/test/Text/Ginger/SimulationTests.hs
@@ -904,6 +904,20 @@
                     "{{ \"12345\"|slice(1,3) }}"
                     "234"
             ]
+        , testGroup "\"split\""
+            [ testCase "word-splitting" $ do
+                mkTestText [] []
+                    "{{ split('foo bar')|json(pretty=0) }}"
+                    "[\"foo\",\"bar\"]"
+            , testCase "word-splitting, more spaces" $ do
+                mkTestText [] []
+                    "{{ split('  foo  bar ')|json(pretty=0) }}"
+                    "[\"foo\",\"bar\"]"
+            , testCase "word-splitting, more spaces, explicit delim" $ do
+                mkTestText [] []
+                    "{{ split('  foo  bar ','')|json(pretty=0) }}"
+                    "[\"foo\",\"bar\"]"
+            ]
         , testGroup "\"replace\""
             [ testCase "simple case" $ do
                 mkTestHtml [] []
