diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for layout-rules
+
+## 0.1.0.0  -- 2017-03-21
+
+* First version. Off-sides implementation extracted from the `located` package.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2017, Trevor Elliott
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Trevor Elliott nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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/layout-rules.cabal b/layout-rules.cabal
new file mode 100644
--- /dev/null
+++ b/layout-rules.cabal
@@ -0,0 +1,30 @@
+name:                layout-rules
+version:             0.1.0.0
+synopsis:            A collection of different layout implementations
+homepage:            https://github.com/elliottt/layout-rules
+license:             BSD3
+license-file:        LICENSE
+author:              Trevor Elliott
+maintainer:          awesomelyawesome@gmail.com
+copyright:           Trevor Elliott, 2017
+category:            Language
+build-type:          Simple
+extra-source-files:  ChangeLog.md
+cabal-version:       >=1.10
+
+description:
+  A collection of different layout implementations, currently just the off-sides
+  rule.
+
+source-repository head
+  type: git
+  location: https://github.com/elliottt/layout-rules
+
+library
+  exposed-modules:     Text.Layout.OffSides
+  build-depends:       base >=4.7 && < 5,
+                       alex-tools >= 0.1.1.0 && < 0.2,
+                       text >= 1.2 && < 1.3
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  ghc-options:         -Wall
diff --git a/src/Text/Layout/OffSides.hs b/src/Text/Layout/OffSides.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Layout/OffSides.hs
@@ -0,0 +1,82 @@
+
+{-# LANGUAGE RecordWildCards #-}
+
+module Text.Layout.OffSides (
+
+    -- * Off-sides Rule
+    Layout(..),
+    layout,
+
+    -- ** Utilities
+    wrapToken,
+  ) where
+
+import AlexTools
+import qualified Data.Text as T
+
+
+data Layout tok = Layout { beginsLayout :: tok -> Bool
+                           -- ^ True when this token begins layout
+
+                         , endsLayout :: tok -> Bool
+                           -- ^ True when this token explicitly ends layout
+
+                         , sep :: SourceRange -> Lexeme tok
+                           -- ^ The separator token
+
+                         , start :: SourceRange -> Lexeme tok
+                           -- ^ Layout block starting token
+
+                         , end :: SourceRange -> Lexeme tok
+                           -- ^ Layout block ending token
+                         }
+
+
+-- | Turn a single token into the form required by the 'Layout' type.
+wrapToken :: tok -> SourceRange -> Lexeme tok
+wrapToken lexemeToken lexemeRange = Lexeme { lexemeText = T.empty, .. }
+
+
+-- | The off-sides rule.
+layout :: Layout tok -> [Lexeme tok] -> [Lexeme tok]
+layout Layout { .. } = go Nothing []
+  where
+  startCol SourceRange { sourceFrom = SourcePos { .. } } = sourceColumn
+
+  currentLevel (loc : _) = startCol loc
+  currentLevel []        = 0
+
+  -- a new layout level has been started, emit a starting token, and push the
+  -- current level on the stack.
+  go Just{} stack (tok : toks) =
+    let loc = lexemeRange tok
+     in start loc : tok : go Nothing (loc:stack) toks
+
+  go (Just loc) stack [] =
+    start loc : go Nothing (loc : stack) []
+
+  go Nothing stack ts@(tok : toks)
+
+    -- when the next token would close the current level
+    | startCol loc < currentLevel stack =
+      end loc : go Nothing (tail stack) ts
+
+    | beginsLayout (lexemeToken tok) =
+      let sepToks | startCol loc == currentLevel stack = [sep loc]
+                  | otherwise                          = []
+       in sepToks ++ tok : go (Just loc) stack toks
+
+    | endsLayout (lexemeToken tok) =
+      end loc : tok : go Nothing (tail stack) toks
+
+    | startCol loc == currentLevel stack =
+      sep loc : tok : go Nothing stack toks
+
+    | otherwise =
+      tok : go Nothing stack toks
+
+    where
+    loc = lexemeRange tok
+
+  go _ stack [] =
+    [ end loc | loc <- stack ]
