diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for hslua-module-text
+
+## 0.1  -- 2017-11-15
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Albert Krewinkel
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/hslua-module-text.cabal b/hslua-module-text.cabal
new file mode 100644
--- /dev/null
+++ b/hslua-module-text.cabal
@@ -0,0 +1,40 @@
+name:                hslua-module-text
+version:             0.1
+synopsis:            Lua module for text
+description:         UTF-8 aware subset of Lua's `string` module.
+homepage:            https://github.com/hslua/hslua-text-module
+license:             MIT
+license-file:        LICENSE
+author:              Albert Krewinkel
+maintainer:          albert+hslua@zeitkraut.de
+copyright:           © 2017 Albert Krewinkel
+category:            Foreign
+build-type:          Simple
+extra-source-files:  ChangeLog.md
+cabal-version:       >=1.10
+
+source-repository head
+  type:              git
+  location:          https://github.com/hslua/hslua-module-text.git
+
+library
+  exposed-modules:     Foreign.Lua.Module.Text
+  build-depends:       base  >= 4.8 && < 4.11
+                     , hslua >= 0.9 && < 0.10
+                     , text  >= 1   && < 1.3
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+test-suite test-hslua
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  main-is:             test-hslua-module-text.hs
+  hs-source-dirs:      test
+  ghc-options:         -Wall -threaded
+  -- other-modules:       Foreign.Lua.Module.TextTest
+  build-depends:       base
+                     , hslua
+                     , hslua-module-text
+                     , tasty
+                     , tasty-hunit
+                     , text
diff --git a/src/Foreign/Lua/Module/Text.hs b/src/Foreign/Lua/Module/Text.hs
new file mode 100644
--- /dev/null
+++ b/src/Foreign/Lua/Module/Text.hs
@@ -0,0 +1,95 @@
+{-
+Copyright © 2017 Albert Krewinkel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+-}
+
+{-|
+Module      : Foreign.Lua.Module.Text
+Copyright   : © 2017 Albert Krewinkel
+License     : MIT
+Maintainer  : Albert Krewinkel <tarleb+hslua@zeitkraut.de>
+Stability   : alpha
+Portability : ForeignFunctionInterface
+
+Provide a lua module containing a selection of useful Text functions.
+-}
+module Foreign.Lua.Module.Text
+  ( pushModuleText
+  , preloadTextModule
+  )where
+
+import Data.Text (Text)
+import Data.Maybe (fromMaybe)
+import Foreign.Lua (FromLuaStack, NumResults, Lua, LuaInteger, ToLuaStack)
+import Foreign.Lua.FunctionCalling (ToHaskellFunction, newCFunction)
+import qualified Foreign.Lua as Lua
+import qualified Data.Text as T
+
+-- | Pushes the @text@ module to the lua stack.
+pushModuleText :: Lua NumResults
+pushModuleText = do
+  Lua.newtable
+  addFunction "lower" (return . T.toLower :: Text -> Lua Text)
+  addFunction "upper" (return . T.toUpper :: Text -> Lua Text)
+  addFunction "reverse" (return . T.reverse :: Text -> Lua Text)
+  addFunction "len" (return . fromIntegral . T.length :: Text -> Lua LuaInteger)
+  addFunction "sub" sub
+  return 1
+
+-- | Add the text module under the given name to the table of preloaded
+-- packages.
+preloadTextModule :: String -> Lua ()
+preloadTextModule = flip addPackagePreloader pushModuleText
+
+-- | Registers a preloading function. Takes an module name and the Lua operation
+-- which produces the package.
+addPackagePreloader :: String -> Lua NumResults -> Lua ()
+addPackagePreloader name modulePusher = do
+  Lua.getglobal' "package.preload"
+  Lua.pushcfunction =<< newCFunction modulePusher
+  Lua.setfield (-2) name
+  Lua.pop 1
+
+-- | Attach a function to the table at the top of the stack, using the given
+-- name.
+addFunction :: ToHaskellFunction a => String -> a -> Lua ()
+addFunction name fn = do
+  Lua.push name
+  Lua.pushHaskellFunction fn
+  Lua.rawset (-3)
+
+-- | Returns a substring, using Lua's string indexing rules.
+sub :: Text -> LuaInteger -> OrNil LuaInteger -> Lua Text
+sub s i j =
+  let i' = fromIntegral i
+      j' = fromIntegral . fromMaybe (-1) $ toMaybe j
+      fromStart = if i' >= 0 then  i' - 1 else T.length s + i'
+      fromEnd   = if j' <  0 then -j' - 1 else T.length s - j'
+  in return . T.dropEnd fromEnd . T.drop fromStart $ s
+
+-- A lua value or nil
+newtype OrNil a = OrNil { toMaybe :: Maybe a }
+
+instance FromLuaStack a => FromLuaStack (OrNil a) where
+  peek idx = do
+    noValue <- Lua.isnoneornil idx
+    if noValue
+      then return (OrNil Nothing)
+      else OrNil . Just <$> Lua.peek idx
diff --git a/test/test-hslua-module-text.hs b/test/test-hslua-module-text.hs
new file mode 100644
--- /dev/null
+++ b/test/test-hslua-module-text.hs
@@ -0,0 +1,62 @@
+{-
+Copyright © 2017 Albert Krewinkel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+-}
+import Control.Monad (void, when)
+import Foreign.Lua (Lua, runLua)
+import Foreign.Lua.Module.Text (preloadTextModule, pushModuleText)
+import Test.Tasty (TestTree, defaultMain, testGroup)
+import Test.Tasty.HUnit (assertEqual, testCase)
+
+import qualified Foreign.Lua as Lua
+
+main :: IO ()
+main = defaultMain $ testGroup "hslua-module-text" [tests]
+
+-- | HSpec tests
+tests :: TestTree
+tests = testGroup "FromLuaStack"
+  [ testCase "text module can be pushed to the stack" $
+      runLua (void pushModuleText)
+
+  , testCase "text module can be added to the preloader" . runLua $ do
+      Lua.openlibs
+      preloadTextModule "hstext"
+      assertEqual' "function not added to preloader" Lua.TypeFunction =<< do
+        Lua.getglobal' "package.preload.hstext"
+        Lua.ltype (-1)
+
+  , testCase "text module can be loaded as hstext" . runLua $ do
+      Lua.openlibs
+      preloadTextModule "hstext"
+      assertEqual' "loading the module fails " Lua.OK =<<
+        Lua.dostring "require 'hstext'"
+
+  , testCase "Lua tests pass" . runLua $ do
+      Lua.openlibs
+      preloadTextModule "hstext"
+      assertEqual' "error while running lua tests" Lua.OK =<< do
+        st <- Lua.loadfile "test/hstext-test.lua"
+        when (st == Lua.OK) $ Lua.call 0 0
+        return st
+  ]
+
+assertEqual' :: (Show a, Eq a) => String -> a -> a -> Lua ()
+assertEqual' msg expected = Lua.liftIO . assertEqual msg expected
