diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@
   the assumption is that the calling program will escape
   the strings appropriately for the output format.)
 - If the value of the variable is a boolean value, it
-  will be rendered as `true` if true, or as empty if false.
+  will be rendered as `true` if true, or as `false` if false.
 - If the value is a list, the values will be concatenated.
 - If the value is a map, the string `true` will be rendered.
 - Every other value will be rendered as the empty string.
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,12 +1,25 @@
 # doctemplates
 
-# 0.11.0.1
+## 0.11.1
 
+  * Add `getVariables`, returning free variables of a template [API change].
+    Returns variable names in order of first occurrence, without
+    duplicates, excluding variables bound inside for loops (the loop
+    variable, fields underneath it, and the anaphoric variable `it`).
+
+  * Fix off-by-one error in `alpha` pipe.
+
+  * Fix `roman` pipe for numbers >= 4000.
+
+  * Fix `reverse`, `alpha`, `roman` pipes on multi-chunk Docs.
+
+## 0.11.0.1
+
   * Bump version bounds for doclayout.
 
   * Fix typos.
 
-# 0.11
+## 0.11
 
   * Remove HsYAML depenedency.
 
diff --git a/doctemplates.cabal b/doctemplates.cabal
--- a/doctemplates.cabal
+++ b/doctemplates.cabal
@@ -1,5 +1,5 @@
 name:                doctemplates
-version:             0.11.0.1
+version:             0.11.1
 synopsis:            Pandoc-style document templates
 description:         This is the text templating system used by pandoc.
                      It supports variable interpolation, iteration,
@@ -15,13 +15,12 @@
 copyright:           2016-19 John MacFarlane
 category:            Text
 build-type:          Simple
--- extra-source-files:
-data-files:          README.md
+extra-doc-files:     README.md
                      changelog.md
 extra-source-files:  test/*.test
                      test/*.txt
                      test/*.tex
-cabal-version:       >=1.10
+cabal-version:       2.0
 
 library
   hs-source-dirs:      src
@@ -33,8 +32,8 @@
                        text-conversions,
                        aeson,
                        text,
-                       doclayout >= 0.4 && < 0.6,
-                       containers,
+                       doclayout >= 0.4 && < 0.7,
+                       containers >= 0.6.0.1,
                        vector,
                        filepath,
                        parsec,
@@ -51,7 +50,7 @@
   main-is:             test.hs
   build-depends:       base,
                        doctemplates,
-                       doclayout >= 0.4 && < 0.6,
+                       doclayout >= 0.4 && < 0.7,
                        containers,
                        aeson,
                        Glob,
@@ -70,7 +69,7 @@
   Main-Is:         bench.hs
   Hs-Source-Dirs:  bench
   Build-Depends:   doctemplates,
-                   doclayout >= 0.4 && < 0.6,
+                   doclayout >= 0.4 && < 0.7,
                    base >= 4.8 && < 5,
                    criterion >= 1.0,
                    filepath,
diff --git a/src/Text/DocTemplates.hs b/src/Text/DocTemplates.hs
--- a/src/Text/DocTemplates.hs
+++ b/src/Text/DocTemplates.hs
@@ -105,7 +105,7 @@
     calling program will escape the strings appropriately for the output
     format.)
 -   If the value of the variable is a boolean value, it will be rendered
-    as @true@ if true, or as empty if false.
+    as @true@ if true, or as @false@ if false.
 -   If the value is a list, the values will be concatenated.
 -   If the value is a map, the string @true@ will be rendered.
 -   Every other value will be rendered as the empty string.
@@ -405,6 +405,7 @@
 -}
 
 module Text.DocTemplates ( renderTemplate
+                         , getVariables
                          , compileTemplate
                          , compileTemplateFile
                          , applyTemplate
@@ -424,7 +425,7 @@
 import Text.DocTemplates.Parser (compileTemplate)
 import Text.DocTemplates.Internal ( TemplateMonad(..), Context(..),
             Val(..), ToContext(..), FromContext(..), TemplateTarget,
-            Template, renderTemplate )
+            Template, renderTemplate, getVariables )
 
 -- | Compile a template from a file.  IO errors will be
 -- raised as exceptions; template parsing errors result in
diff --git a/src/Text/DocTemplates/Internal.hs b/src/Text/DocTemplates/Internal.hs
--- a/src/Text/DocTemplates/Internal.hs
+++ b/src/Text/DocTemplates/Internal.hs
@@ -22,6 +22,7 @@
 
 module Text.DocTemplates.Internal
       ( renderTemplate
+      , getVariables
       , TemplateMonad(..)
       , Context(..)
       , Val(..)
@@ -40,7 +41,6 @@
 import Control.Monad.Identity
 import qualified Control.Monad.State.Strict as S
 import Data.Char (chr, ord)
-import Data.Maybe (fromMaybe)
 import qualified Data.Text.Read as T
 import qualified Data.Text as T
 import qualified Data.Text.IO as TIO
@@ -52,9 +52,10 @@
 import GHC.Generics (Generic)
 import Data.Text (Text)
 import qualified Data.Map as M
+import Data.Containers.ListUtils (nubOrd)
 import qualified Data.Vector as V
 import Data.Scientific (floatingOrInteger)
-import Data.List (intersperse)
+import Data.List (intersperse, isPrefixOf)
 #if MIN_VERSION_base(4,11,0)
 #else
 import Data.Semigroup
@@ -252,6 +253,11 @@
     BoolVal b          -> BoolVal b
     NullVal            -> NullVal
 
+-- | The full rendered text of a 'Doc', regardless of how it is
+-- divided into chunks.
+renderedText :: TemplateTarget a => Doc a -> Text
+renderedText = toText . DL.render Nothing
+
 mapText :: TemplateTarget a => (Text -> Text) -> Val a -> Val a
 mapText f val =
   runIdentity (traverse (return . fromText . f . toText) val)
@@ -297,19 +303,22 @@
 applyPipe Reverse val =
   case val of
     ListVal xs  -> ListVal (reverse xs)
-    SimpleVal{} -> mapText T.reverse val
+    SimpleVal d -> SimpleVal $ DL.literal . fromText . T.reverse $
+                     renderedText d
     _           -> val
 applyPipe Chomp val = mapDoc DL.chomp val
-applyPipe ToAlpha val = mapText toAlpha val
-  where toAlpha t =
-          case T.decimal t of
-            Right (y,"") -> fromString [chr (ord 'a' + (y `mod` 26) - 1)]
-            _            -> t
-applyPipe ToRoman val = mapText toRoman' val
-  where toRoman' t =
-         case T.decimal t of
-           Right (y,"") -> fromMaybe t (toRoman y)
-           _            -> t
+applyPipe ToAlpha val = mapDoc toAlpha val
+  where toAlpha d =
+          case T.decimal (renderedText d) of
+            Right (y,"")
+              | y > (0 :: Int) -> fromString
+                                    [chr (ord 'a' + ((y - 1) `mod` 26))]
+            _            -> d
+applyPipe ToRoman val = mapDoc toRoman' val
+  where toRoman' d =
+         case T.decimal (renderedText d) of
+           Right (y,"") -> maybe d (DL.literal . fromText) (toRoman y)
+           _            -> d
 applyPipe NoWrap val = mapDoc DL.nowrap val
 applyPipe (Block align n border) val =
   let constructor = case align of
@@ -333,8 +342,8 @@
 -- | Convert number 0 < x < 4000 to lowercase roman numeral.
 toRoman :: Int -> Maybe Text
 toRoman x
-  | x >= 1000
-  , x < 4000  = ("m" <>) <$> toRoman (x - 1000)
+  | x >= 4000 = Nothing
+  | x >= 1000 = ("m" <>) <$> toRoman (x - 1000)
   | x >= 900  = ("cm" <>) <$> toRoman (x - 900)
   | x >= 500  = ("d" <>) <$> toRoman (x - 500)
   | x >= 400  = ("cd" <>) <$> toRoman (x - 400)
@@ -424,6 +433,28 @@
 renderTemplate :: (TemplateTarget a, ToContext a b)
                => Template a -> b -> Doc a
 renderTemplate t x = S.evalState (renderTemp t (toContext x)) 0
+
+-- | Retrieve the names of all free variables used in a template,
+-- in the order in which they first occur, without duplicates.
+-- The parts of a multipart variable (@foo.bar@) are separated
+-- by periods.  Variables bound inside a @for@ loop (the loop
+-- variable, fields underneath it, and the anaphoric variable
+-- @it@) are not included.
+getVariables :: Template a -> [Text]
+getVariables = nubOrd . map (T.intercalate ".") . go
+ where
+  go (Interpolate v)       = [varParts v]
+  go (Conditional v t1 t2) = varParts v : go t1 <> go t2
+  go (Iterate v t1 t2)     = varParts v :
+                             filter (not . boundBy v) (go t1) <> go t2
+  go (Nested t)            = go t
+  go (Partial _ t)         = go t
+  go (Literal _)           = []
+  go (Concat t1 t2)        = go t1 <> go t2
+  go Empty                 = []
+  -- inside the body of @for(v)@, variables under v are rebound
+  -- to the current list item, and "it" refers to the item itself
+  boundBy v parts = varParts v `isPrefixOf` parts || take 1 parts == ["it"]
 
 updateColumn :: TemplateTarget a => Doc a -> RenderState (Doc a)
 updateColumn x = do
diff --git a/test/test.hs b/test/test.hs
--- a/test/test.hs
+++ b/test/test.hs
@@ -80,6 +80,40 @@
                        :: Val T.Text) mempty))
                   Left e  -> T.pack e
       res @?= "hello this is a test of the wrapping\nhello this\nis a test\nof the\nwrapping"
+  , testCase "alpha pipe" $ do
+      (templ :: Either String (Template T.Text)) <-
+        compileTemplate "foo" "$one/alpha$ $twentysix/alpha$ $twentyseven/alpha$ $zero/alpha$"
+      let res :: T.Text
+          res = case templ of
+                  Right t -> render Nothing
+                   (renderTemplate t (object [ "one" .= (1 :: Int)
+                                             , "twentysix" .= (26 :: Int)
+                                             , "twentyseven" .= (27 :: Int)
+                                             , "zero" .= (0 :: Int) ]))
+                  Left e  -> T.pack e
+      res @?= "a z a 0"
+  , testCase "getVariables" $ do
+      (res :: Either String (Template T.Text)) <-
+        compileTemplate "" "$b$$a$$if(foo.bar)$$b$$else$$baz$$endif$"
+      fmap getVariables res @?= Right ["b", "a", "foo.bar", "baz"]
+  , testCase "getVariables excludes loop-bound variables" $ do
+      (res :: Either String (Template T.Text)) <-
+        compileTemplate ""
+          "$for(employee)$$employee.name$: $it.salary$ $company$$sep$$separator$$endfor$"
+      fmap getVariables res @?= Right ["employee", "company", "separator"]
+  , testCase "getVariables excludes it in bracketed loop" $ do
+      (res :: Either String (Template T.Text)) <-
+        compileTemplate "" "$items[, ]$"
+      fmap getVariables res @?= Right ["items"]
+  , testCase "getVariables with nested loops" $ do
+      (res :: Either String (Template T.Text)) <-
+        compileTemplate ""
+          "$for(a)$$for(a.b)$$it$$a.c$$x$$endfor$$endfor$"
+      fmap getVariables res @?= Right ["a", "x"]
+  , testCase "getVariables includes it outside of loops" $ do
+      (res :: Either String (Template T.Text)) <-
+        compileTemplate "" "$it$"
+      fmap getVariables res @?= Right ["it"]
   ]
 
 {- The test "golden" files are structured as follows:
