diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,43 @@
 
 `pandoc-lua-marshal` uses [PVP Versioning][].
 
+## 0.1.3
+
+Release pending.
+
+### Lua changes
+
+-   The traversal order of filters can now be selected by setting
+    the key `traverse` to either `'topdown'` or `'typewise'`; the
+    default remains `'typewise'`.
+
+    Topdown traversals can be cut short by returning `false` as a
+    second value from the filter function. No child-element of
+    the returned element is processed in that case.
+
+-   All types can be compared. Previously, comparing values of
+    different types would lead to errors in a number of cases.
+
+-   Lists now have an `__eq` metamethod. List equality is checked
+    by comparing both lists element-wise. Two lists are equal if
+    they have the same type and have equal elements.
+
+-   If start indices in `List:find` and `List:find_if` are
+    negative the start index is relative to the list length.
+
+-   TableFoot, TableHead, and Row values are marshaled as
+    userdata objects.
+
+### Haskell code
+
+-   Text.Pandoc.Lua.Marshal.Filter exports the new type
+    `WalkingOrder`. The type `Filter` now contains the the
+    traversal specifier as a field.
+
+-   New modules for TableFoot, TableHead, and Row, defining the
+    usual marshaling methods and constructor functions for these
+    types.
+
 ## 0.1.2
 
 Released 2021-12-10.
diff --git a/cbits/listmod.c b/cbits/listmod.c
--- a/cbits/listmod.c
+++ b/cbits/listmod.c
@@ -5,6 +5,13 @@
 
 #define LIST_T "List"
 
+/* compatibility with older Lua versions, which did not define this in the
+ * header. */
+#ifndef LUA_LOADED_TABLE
+/* key, in the registry, for table of loaded modules */
+#define LUA_LOADED_TABLE	"_LOADED"
+#endif
+
 /*
 ** Placeholder function.
 */
@@ -14,7 +21,7 @@
   );
 }
 
-/* translate a relative table position: negative means back from end */
+/* Translate a relative table position: negative means back from end */
 static lua_Integer posrelat (lua_Integer pos, size_t len) {
   if (pos >= 0) return pos;
   else if (0u - (size_t)pos > len) return 0;
@@ -83,6 +90,42 @@
 }
 
 /*
+** Checks equality. Two lists are equal if and only if they have the same
+** metatable and if all items are equal.
+*/
+static int list_eq (lua_State *L) {
+  lua_settop(L, 2);
+  /* compare meta tables */
+  if (!(lua_getmetatable(L, 1) &&
+        lua_getmetatable(L, 2) &&
+        lua_rawequal(L, -1, -2))) {
+    lua_pushboolean(L, 0);
+    return 1;
+  };
+  lua_pop(L, 2);  /* remove metatables */
+
+  /* ensure both lists have the same length */
+  lua_Integer len1 = luaL_len(L, 1);
+  lua_Integer len2 = luaL_len(L, 2);
+  if (len1 != len2) {
+    lua_pushboolean(L, 0);
+    return 1;
+  }
+
+  /* check element-wise equality  */
+  for (lua_Integer i = 1; i <= len1; i++) {
+    lua_geti(L, 1, i);
+    lua_geti(L, 2, i);
+    if (!lua_compare(L, -1, -2, LUA_OPEQ)) {
+      lua_pushboolean(L, 0);
+      return 1;
+    }
+  }
+  lua_pushboolean(L, 1);
+  return 1;
+}
+
+/*
 ** Appends the second list to the first.
 */
 static int list_extend (lua_State *L) {
@@ -131,8 +174,8 @@
 static int list_find (lua_State *L) {
   lua_settop(L, 3);
   luaL_checktype(L, 1, LUA_TTABLE);
-  lua_Integer start = luaL_optinteger(L, 3, 1);
   lua_Integer len = luaL_len(L, 1);
+  lua_Integer start = posrelat(luaL_optinteger(L, 3, 1), len);
   for (lua_Integer i = start; i <= len; i++) {
     lua_geti(L, 1, i);
     if (lua_compare(L, 2, -1, LUA_OPEQ)) {
@@ -153,8 +196,8 @@
   lua_settop(L, 3);
   luaL_checktype(L, 1, LUA_TTABLE);
   luaL_checktype(L, 2, LUA_TFUNCTION);
-  lua_Integer start = luaL_optinteger(L, 3, 1);
   lua_Integer len = luaL_len(L, 1);
+  lua_Integer start = posrelat(luaL_optinteger(L, 3, 1), len);
   for (lua_Integer i = start; i <= len; i++) {
     lua_pushvalue(L, 2);  /* predicate function */
     lua_geti(L, 1, i);
@@ -194,7 +237,7 @@
   luaL_checktype(L, 2, LUA_TFUNCTION);
   lua_Integer len = luaL_len(L, 1);
   lua_createtable(L, len, 0);  /* create new table */
-  lua_getmetatable(L, 1);
+  luaL_getmetatable(L, LIST_T);  /* make result a generic list */
   lua_setmetatable(L, 3);
   for (lua_Integer i = 1; i <= len; i++) {
     lua_pushvalue(L, 2);  /* map function */
@@ -250,6 +293,7 @@
 
 static const luaL_Reg list_funcs[] = {
   {"__concat", list_concat},
+  {"__eq", list_eq},
   {"clone", list_clone},
   {"extend", list_extend},
   {"filter", list_filter},
diff --git a/pandoc-lua-marshal.cabal b/pandoc-lua-marshal.cabal
--- a/pandoc-lua-marshal.cabal
+++ b/pandoc-lua-marshal.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 name:                pandoc-lua-marshal
-version:             0.1.2
+version:             0.1.3
 synopsis:            Use pandoc types in Lua
 description:         This package provides functions to marshal and unmarshal
                      pandoc document types to and from Lua.
@@ -91,9 +91,13 @@
                      , Text.Pandoc.Lua.Marshal.MetaValue
                      , Text.Pandoc.Lua.Marshal.Pandoc
                      , Text.Pandoc.Lua.Marshal.QuoteType
+                     , Text.Pandoc.Lua.Marshal.Row
                      , Text.Pandoc.Lua.Marshal.SimpleTable
+                     , Text.Pandoc.Lua.Marshal.TableFoot
+                     , Text.Pandoc.Lua.Marshal.TableHead
                      , Text.Pandoc.Lua.Marshal.TableParts
   other-modules:       Text.Pandoc.Lua.Marshal.Shared
+                     , Text.Pandoc.Lua.Topdown
                      , Text.Pandoc.Lua.SpliceList
                      , Text.Pandoc.Lua.Walk
 
diff --git a/src/Text/Pandoc/Lua/Marshal/Attr.hs b/src/Text/Pandoc/Lua/Marshal/Attr.hs
--- a/src/Text/Pandoc/Lua/Marshal/Attr.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Attr.hs
@@ -38,9 +38,9 @@
 typeAttr :: LuaError e => DocumentedType e Attr
 typeAttr = deftype "Attr"
   [ operation Eq $ lambda
-    ### liftPure2 (==)
-    <#> parameter peekAttr "a1" "Attr" ""
-    <#> parameter peekAttr "a2" "Attr" ""
+    ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+    <#> parameter (optional . peekAttr) "a" "Attr" ""
+    <#> parameter (optional . peekAttr) "b" "Attr" ""
     =#> functionResult pushBool "boolean" "whether the two are equal"
   , operation Tostring $ lambda
     ### liftPure show
diff --git a/src/Text/Pandoc/Lua/Marshal/Block.hs b/src/Text/Pandoc/Lua/Marshal/Block.hs
--- a/src/Text/Pandoc/Lua/Marshal/Block.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Block.hs
@@ -25,7 +25,7 @@
   , walkBlocksStraight
   ) where
 
-import Control.Applicative ((<|>))
+import Control.Applicative ((<|>), optional)
 import Control.Monad.Catch (throwM)
 import Control.Monad ((<$!>))
 import Data.Data (showConstr, toConstr)
@@ -110,9 +110,9 @@
 typeBlock :: forall e. LuaError e => DocumentedType e Block
 typeBlock = deftype "Block"
   [ operation Eq $ lambda
-    ### liftPure2 (==)
-    <#> parameter peekBlockFuzzy "Block" "a" ""
-    <#> parameter peekBlockFuzzy "Block" "b" ""
+    ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+    <#> parameter (optional . peekBlockFuzzy) "Block" "a" ""
+    <#> parameter (optional . peekBlockFuzzy) "Block" "b" ""
     =#> boolResult "whether the two values are equal"
   , operation Tostring $ lambda
     ### liftPure show
diff --git a/src/Text/Pandoc/Lua/Marshal/Citation.hs b/src/Text/Pandoc/Lua/Marshal/Citation.hs
--- a/src/Text/Pandoc/Lua/Marshal/Citation.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Citation.hs
@@ -38,7 +38,7 @@
              => DocumentedType e Citation
 typeCitation = deftype "Citation"
   [ operation Eq $ lambda
-    ### liftPure2 (==)
+    ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
     <#> parameter (optional . peekCitation) "Citation" "a" ""
     <#> parameter (optional . peekCitation) "Citation" "b" ""
     =#> functionResult pushBool "boolean" "true iff the citations are equal"
diff --git a/src/Text/Pandoc/Lua/Marshal/Filter.hs b/src/Text/Pandoc/Lua/Marshal/Filter.hs
--- a/src/Text/Pandoc/Lua/Marshal/Filter.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Filter.hs
@@ -13,6 +13,7 @@
 module Text.Pandoc.Lua.Marshal.Filter
   ( -- * Filters
     Filter (..)
+  , WalkingOrder (..)
   , peekFilter
   , lookup
   , member
@@ -28,7 +29,7 @@
   ) where
 
 import Prelude hiding (lookup)
-import Control.Applicative ((<|>))
+import Control.Applicative ((<|>), optional)
 import Control.Monad ((<$!>))
 import Data.Data
   ( Data, dataTypeConstrs, dataTypeName, dataTypeOf
@@ -63,8 +64,18 @@
 
 -- | Collection of filter functions (at most one function per element
 -- constructor)
-newtype Filter = Filter (Map Name FilterFunction)
+data Filter = Filter
+  { filterWalkingOrder :: WalkingOrder
+  , filterMap :: Map Name FilterFunction
+  }
 
+-- | Description of how an AST should be traversed.
+data WalkingOrder
+  = WalkForEachType  -- ^ Process each type separately, traversing the
+                     -- tree bottom-up (leaves to root) for each type.
+  | WalkTopdown      -- ^ Traverse the tree top-down, from root to
+                     -- leaves and depth first, in a single traversal.
+
 -- | Retrieves a default `Filter` object from the stack, suitable for
 -- filtering a full document.
 peekFilter :: LuaError e => Peeker e Filter
@@ -87,15 +98,21 @@
         runPeek (peekFilterFunction top `lastly` pop 1) >>= \case
           Success fn -> pure $ Map.insert constr fn acc
           Failure {} -> pure acc
-  Filter <$!> foldrM go Map.empty fnNames
+  walkingSequence <- do
+    _ <- liftLua $ getfield idx "traverse"
+    optional (peekText top) `lastly` pop 1 >>= \case
+      Just "typewise" -> pure WalkForEachType
+      Just "topdown"  -> pure WalkTopdown
+      _               -> pure WalkForEachType
+  Filter walkingSequence <$!> foldrM go Map.empty fnNames
 
 -- | Looks up a filter function in a Lua 'Filter'.
 lookup :: Name -> Filter -> Maybe FilterFunction
-lookup name (Filter filterMap) = name `Map.lookup` filterMap
+lookup name = (name `Map.lookup`) . filterMap
 
 -- | Checks whether the 'Filter' contains a function of the given name.
 member :: Name -> Filter -> Bool
-member name (Filter filterMap) = name `Map.member` filterMap
+member name = (name `Map.member`) . filterMap
 
 -- | Filter function names for a given type.
 valueFunctionNames :: forall a. Data a => Proxy a -> [Name]
@@ -115,6 +132,8 @@
   fromString . (++ "s") . tyconUQname . dataTypeName . dataTypeOf
   $ (undefined :: a)
 
+-- | Finds the best filter function for a given element; returns
+-- 'Nothing' if no such function exists.
 getFunctionFor :: forall a. Data a => Filter -> a -> Maybe FilterFunction
 getFunctionFor filter' x =
   let constrName = fromString . showConstr . toConstr $ x
diff --git a/src/Text/Pandoc/Lua/Marshal/Inline.hs b/src/Text/Pandoc/Lua/Marshal/Inline.hs
--- a/src/Text/Pandoc/Lua/Marshal/Inline.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Inline.hs
@@ -25,6 +25,7 @@
   , walkInlinesStraight
   ) where
 
+import Control.Applicative (optional)
 import Control.Monad.Catch (throwM)
 import Control.Monad ((<$!>))
 import Data.Data (showConstr, toConstr)
@@ -106,9 +107,9 @@
     <#> parameter peekInline "inline" "Inline" "Object"
     =#> functionResult pushString "string" "stringified Inline"
   , operation Eq $ defun "__eq"
-      ### liftPure2 (==)
-      <#> parameter peekInline "a" "Inline" ""
-      <#> parameter peekInline "b" "Inline" ""
+      ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+      <#> parameter (optional . peekInline) "a" "Inline" ""
+      <#> parameter (optional . peekInline) "b" "Inline" ""
       =#> functionResult pushBool "boolean" "whether the two are equal"
   ]
   [ possibleProperty "attr" "element attributes"
diff --git a/src/Text/Pandoc/Lua/Marshal/ListAttributes.hs b/src/Text/Pandoc/Lua/Marshal/ListAttributes.hs
--- a/src/Text/Pandoc/Lua/Marshal/ListAttributes.hs
+++ b/src/Text/Pandoc/Lua/Marshal/ListAttributes.hs
@@ -19,6 +19,7 @@
   , pushListNumberStyle
   ) where
 
+import Control.Applicative (optional)
 import Data.Maybe (fromMaybe)
 import HsLua
 import Text.Pandoc.Definition
@@ -28,9 +29,9 @@
 typeListAttributes :: LuaError e => DocumentedType e ListAttributes
 typeListAttributes = deftype "ListAttributes"
   [ operation Eq $ lambda
-    ### liftPure2 (==)
-    <#> parameter peekListAttributes "a" "ListAttributes" ""
-    <#> parameter peekListAttributes "b" "ListAttributes" ""
+    ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+    <#> parameter (optional . peekListAttributes) "a" "ListAttributes" ""
+    <#> parameter (optional . peekListAttributes) "b" "ListAttributes" ""
     =#> functionResult pushBool "boolean" "whether the two are equal"
   ]
   [ property "start" "number of the first list item"
diff --git a/src/Text/Pandoc/Lua/Marshal/MetaValue.hs b/src/Text/Pandoc/Lua/Marshal/MetaValue.hs
--- a/src/Text/Pandoc/Lua/Marshal/MetaValue.hs
+++ b/src/Text/Pandoc/Lua/Marshal/MetaValue.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE LambdaCase           #-}
 {-# LANGUAGE OverloadedStrings    #-}
 {-# LANGUAGE ScopedTypeVariables  #-}
+{-# LANGUAGE TypeApplications     #-}
 {- |
 Copyright               : © 2021 Albert Krewinkel
 SPDX-License-Identifier : MIT
@@ -23,6 +24,7 @@
   ( peekInline, peekInlines, peekInlinesFuzzy, pushInlines )
 import Text.Pandoc.Lua.Marshal.List (pushPandocList)
 import Text.Pandoc.Definition (MetaValue (..))
+import qualified Data.Text as T
 
 -- | Push a 'MetaValue' element to the top of the Lua stack.
 pushMetaValue :: LuaError e => Pusher e MetaValue
@@ -43,6 +45,11 @@
     TypeBoolean -> MetaBool <$!> peekBool idx
 
     TypeString  -> MetaString <$!> peekText idx
+
+    TypeNumber  -> MetaString . T.pack <$>
+      (liftLua (isinteger idx) >>= \case
+          False -> show <$!> peekRealFloat @Double idx
+          True  -> show <$!> peekIntegral @Prelude.Integer idx)
 
     TypeUserdata -> -- Allow singleton Inline or Block elements
       (MetaInlines . (:[]) <$!> peekInline idx) <|>
diff --git a/src/Text/Pandoc/Lua/Marshal/Pandoc.hs b/src/Text/Pandoc/Lua/Marshal/Pandoc.hs
--- a/src/Text/Pandoc/Lua/Marshal/Pandoc.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Pandoc.hs
@@ -26,7 +26,7 @@
 import Text.Pandoc.Lua.Marshal.Block
   (peekBlocksFuzzy, pushBlocks, walkBlockSplicing, walkBlocksStraight)
 import Text.Pandoc.Lua.Marshal.Inline (walkInlineSplicing, walkInlinesStraight)
-import Text.Pandoc.Lua.Marshal.Filter (Filter, peekFilter)
+import Text.Pandoc.Lua.Marshal.Filter
 import Text.Pandoc.Lua.Marshal.MetaValue (peekMetaValue, pushMetaValue)
 import Text.Pandoc.Lua.Marshal.Shared (walkBlocksAndInlines)
 import Text.Pandoc.Lua.Walk (applyStraight)
@@ -44,7 +44,7 @@
 typePandoc :: LuaError e => DocumentedType e Pandoc
 typePandoc = deftype "Pandoc"
   [ operation Eq $ defun "__eq"
-     ### liftPure2 (==)
+     ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
      <#> parameter (optional . peekPandoc) "doc1" "pandoc" ""
      <#> parameter (optional . peekPandoc) "doc2" "pandoc" ""
      =#> functionResult pushBool "boolean" "true iff the two values are equal"
@@ -61,10 +61,13 @@
       (peekMeta, \(Pandoc _ blks) meta -> Pandoc meta blks)
 
   , method $ defun "walk"
-    ### (\doc filter' ->
-               walkBlocksAndInlines filter' doc
-           >>= applyMetaFunction filter'
-           >>= applyPandocFunction filter')
+    ### (\doc filter' -> case filterWalkingOrder filter' of
+            WalkForEachType -> walkBlocksAndInlines filter' doc
+                           >>= applyMetaFunction filter'
+                           >>= applyPandocFunction filter'
+            WalkTopdown     -> applyPandocFunction filter' doc
+                           >>= applyMetaFunction filter'
+                           >>= walkBlocksAndInlines filter')
     <#> parameter peekPandoc "Pandoc" "self" ""
     <#> parameter peekFilter "Filter" "lua_filter" "table of filter functions"
     =#> functionResult pushPandoc "Pandoc" "modified element"
diff --git a/src/Text/Pandoc/Lua/Marshal/Row.hs b/src/Text/Pandoc/Lua/Marshal/Row.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Pandoc/Lua/Marshal/Row.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE LambdaCase           #-}
+{- |
+Copyright               : © 2021 Albert Krewinkel
+SPDX-License-Identifier : MIT
+Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
+
+Marshaling/unmarshaling functions of 'Row' values.
+-}
+module Text.Pandoc.Lua.Marshal.Row
+  ( peekRow
+  , peekRowFuzzy
+  , pushRow
+  , typeRow
+  , mkRow
+  ) where
+
+import Control.Applicative (optional)
+import Control.Monad ((<$!>))
+import Data.Maybe (fromMaybe)
+import HsLua
+import Text.Pandoc.Lua.Marshal.Attr (peekAttr, pushAttr)
+import Text.Pandoc.Lua.Marshal.Cell (peekCellFuzzy, pushCell)
+import Text.Pandoc.Lua.Marshal.Filter (peekFilter)
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Lua.Marshal.Shared (walkBlocksAndInlines)
+import Text.Pandoc.Definition
+
+-- | Push a table Row as a table with fields @attr@, @alignment@,
+-- @row_span@, @col_span@, and @contents@.
+pushRow :: LuaError e => Row -> LuaE e ()
+pushRow = pushUD typeRow
+
+-- | Retrieves a 'Cell' object from the stack.
+peekRow :: LuaError e => Peeker e Row
+peekRow = peekUD typeRow
+
+-- | Retrieves a 'Cell' from the stack, accepting either a 'pandoc Cell'
+-- userdata object or a table with fields @attr@, @alignment@, @row_span@,
+-- @col_span@, and @contents@.
+peekRowFuzzy :: LuaError e => Peeker e Row
+peekRowFuzzy idx = liftLua (ltype idx) >>= \case
+  TypeUserdata -> peekRow idx
+  TypeTable -> uncurry Row <$!> peekPair peekAttr (peekList peekCellFuzzy) idx
+  _ -> failPeek =<< typeMismatchMessage "Cell or table" idx
+
+-- | Row object type.
+typeRow :: LuaError e => DocumentedType e Row
+typeRow = deftype "pandoc Row"
+  [ operation Eq $ defun "__eq"
+     ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+     <#> parameter (optional . peekRow) "Row" "self" ""
+     <#> parameter (optional . peekRow) "any" "object" ""
+     =#> functionResult pushBool "boolean" "true iff the two values are equal"
+  , operation Tostring $ lambda
+    ### liftPure show
+    <#> parameter peekRow "Row" "self" ""
+    =#> functionResult pushString "string" "native Haskell representation"
+  ]
+  [ property "attr" "row attributes"
+      (pushAttr, \(Row attr _) -> attr)
+      (peekAttr, \(Row _ cells) attr ->
+                   Row attr cells)
+  , property "cells" "row cells"
+      (pushPandocList pushCell, \(Row _ cells) -> cells)
+      (peekList peekCellFuzzy, \(Row attr _) cells ->
+                                 Row attr cells)
+
+  , alias "identifier" "cell ID"         ["attr", "identifier"]
+  , alias "classes"    "cell classes"    ["attr", "classes"]
+  , alias "attributes" "cell attributes" ["attr", "attributes"]
+
+  , method $ defun "clone"
+    ### return
+    <#> parameter peekRow "Row" "self" ""
+    =#> functionResult pushRow "Row" "cloned object"
+
+  , method $ defun "walk"
+    ### flip walkBlocksAndInlines
+    <#> parameter peekRow "Row" "self" ""
+    <#> parameter peekFilter "Filter" "lua_filter" "table of filter functions"
+    =#> functionResult pushRow "Row" "modified cell"
+  ]
+
+-- | Constructor function for 'Row' values.
+mkRow :: LuaError e => DocumentedFunction e
+mkRow = defun "Row"
+  ### liftPure2 (\mCells mAttr -> Row
+                  (fromMaybe nullAttr mAttr)
+                  (fromMaybe [] mCells))
+  <#> optionalParameter (peekList peekCellFuzzy) "{Cell,...}" "cells"
+        "row cells"
+  <#> optionalParameter peekAttr "Attr" "attr" "cell attributes"
+  =#> functionResult pushRow "Row" "new Row object"
diff --git a/src/Text/Pandoc/Lua/Marshal/Shared.hs b/src/Text/Pandoc/Lua/Marshal/Shared.hs
--- a/src/Text/Pandoc/Lua/Marshal/Shared.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Shared.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE FlexibleContexts     #-}
+{-# LANGUAGE OverloadedStrings    #-}
 {- |
 Copyright   : © 2021 Albert Krewinkel
 License     : MIT
@@ -11,26 +12,73 @@
     walkBlocksAndInlines
   ) where
 
+import Prelude hiding (lookup)
 import Control.Monad ((>=>))
-import HsLua (LuaE, LuaError)
+import HsLua
 import {-# SOURCE #-} Text.Pandoc.Lua.Marshal.Block
-  ( walkBlockSplicing, walkBlocksStraight )
-import Text.Pandoc.Lua.Marshal.Filter (Filter)
 import {-# SOURCE #-} Text.Pandoc.Lua.Marshal.Inline
-  ( walkInlineSplicing, walkInlinesStraight )
+import Text.Pandoc.Lua.Marshal.Filter
 import Text.Pandoc.Definition
-import Text.Pandoc.Lua.Walk (SpliceList, Walkable)
+import Text.Pandoc.Lua.Topdown
+import Text.Pandoc.Lua.Walk
+import Text.Pandoc.Walk
 
 -- | Walk blocks and inlines.
 walkBlocksAndInlines :: (LuaError e,
                          Walkable (SpliceList Block) a,
                          Walkable (SpliceList Inline) a,
                          Walkable [Block] a,
-                         Walkable [Inline] a)
+                         Walkable [Inline] a,
+                         Walkable Topdown a)
                      => Filter
                      -> a -> LuaE e a
-walkBlocksAndInlines f =
-      walkInlineSplicing f
-  >=> walkInlinesStraight f
-  >=> walkBlockSplicing f
-  >=> walkBlocksStraight f
+walkBlocksAndInlines filter' =
+  case filterWalkingOrder filter' of
+    WalkTopdown     -> walkM (applyFilterTopdown filter')
+    WalkForEachType -> walkInlineSplicing filter'
+                   >=> walkInlinesStraight filter'
+                   >=> walkBlockSplicing filter'
+                   >=> walkBlocksStraight filter'
+
+-- | Applies a filter by processing the root node(s) first and descending
+-- towards the leaves depth-first.
+applyFilterTopdown :: LuaError e
+                   => Filter
+                   -> Topdown -> LuaE e Topdown
+applyFilterTopdown filter' topdown@(Topdown _ node) =
+  case node of
+    TBlock x ->
+      case filter' `getFunctionFor` x of
+        Nothing ->
+          pure topdown
+        Just fn -> do
+          (blocks, ctrl) <-
+            applySplicingFunction fn pushBlock peekBlocksFuzzy x
+          pure $ Topdown ctrl $ TBlocks blocks
+
+    TBlocks xs ->
+      case "Blocks" `lookup` filter' of
+        Nothing ->
+          pure topdown
+        Just fn -> do
+          (blocks, ctrl) <-
+            applyStraightFunction fn pushBlocks peekBlocksFuzzy xs
+          pure $ Topdown ctrl $ TBlocks blocks
+
+    TInline x ->
+      case filter' `getFunctionFor` x of
+        Nothing ->
+          pure topdown
+        Just fn -> do
+          (inlines, ctrl) <-
+            applySplicingFunction fn pushInline peekInlinesFuzzy x
+          pure $ Topdown ctrl $ TInlines inlines
+
+    TInlines xs ->
+      case "Inlines" `lookup` filter' of
+        Nothing ->
+          pure topdown
+        Just fn -> do
+          (inlines, ctrl) <-
+            applyStraightFunction fn pushInlines peekInlinesFuzzy xs
+          pure $ Topdown ctrl $ TInlines inlines
diff --git a/src/Text/Pandoc/Lua/Marshal/SimpleTable.hs b/src/Text/Pandoc/Lua/Marshal/SimpleTable.hs
--- a/src/Text/Pandoc/Lua/Marshal/SimpleTable.hs
+++ b/src/Text/Pandoc/Lua/Marshal/SimpleTable.hs
@@ -17,6 +17,8 @@
   )
   where
 
+import Control.Applicative (optional)
+import Data.Maybe (fromMaybe)
 import HsLua as Lua
 import Text.Pandoc.Lua.Marshal.Alignment (peekAlignment, pushAlignment)
 import Text.Pandoc.Lua.Marshal.Block (peekBlocksFuzzy, pushBlocks)
@@ -36,9 +38,9 @@
 typeSimpleTable :: LuaError e => DocumentedType e SimpleTable
 typeSimpleTable = deftype "SimpleTable"
   [ operation Eq $ lambda
-    ### liftPure2 (==)
-    <#> udparam typeSimpleTable "a" ""
-    <#> udparam typeSimpleTable "b" ""
+    ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+    <#> parameter (optional . peekSimpleTable) "value" "a" ""
+    <#> parameter (optional . peekSimpleTable) "value" "b" ""
     =#> functionResult pushBool "boolean" "whether the two objects are equal"
   , operation Tostring $ lambda
     ### liftPure show
diff --git a/src/Text/Pandoc/Lua/Marshal/TableFoot.hs b/src/Text/Pandoc/Lua/Marshal/TableFoot.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Pandoc/Lua/Marshal/TableFoot.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE OverloadedStrings    #-}
+{- |
+Copyright               : © 2021 Albert Krewinkel
+SPDX-License-Identifier : MIT
+Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
+
+Marshaling/unmarshaling functions of 'TableFoot' values.
+-}
+module Text.Pandoc.Lua.Marshal.TableFoot
+  ( peekTableFoot
+  , pushTableFoot
+  , typeTableFoot
+  , mkTableFoot
+  ) where
+
+import Control.Applicative (optional)
+import Data.Maybe (fromMaybe)
+import HsLua
+import Text.Pandoc.Lua.Marshal.Attr (peekAttr, pushAttr)
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Lua.Marshal.Row (peekRowFuzzy, pushRow)
+import Text.Pandoc.Definition
+
+-- | Push a TableFoot as a userdata value.
+pushTableFoot :: LuaError e => TableFoot -> LuaE e ()
+pushTableFoot = pushUD typeTableFoot
+
+-- | Retrieves a 'Cell' from the stack.
+peekTableFoot :: LuaError e => Peeker e TableFoot
+peekTableFoot = peekUD typeTableFoot
+
+-- | Row object type.
+typeTableFoot :: LuaError e => DocumentedType e TableFoot
+typeTableFoot = deftype "pandoc TableFoot"
+  [ operation Eq $ defun "__eq"
+     ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+     <#> parameter (optional . peekTableFoot) "TableFoot" "self" ""
+     <#> parameter (optional . peekTableFoot) "any" "object" ""
+     =#> functionResult pushBool "boolean" "true iff the two values are equal"
+  , operation Tostring $ lambda
+    ### liftPure show
+    <#> parameter peekTableFoot "TableFoot" "self" ""
+    =#> functionResult pushString "string" "native Haskell representation"
+  ]
+  [ property "attr" "table foot attributes"
+      (pushAttr, \(TableFoot attr _) -> attr)
+      (peekAttr, \(TableFoot _ cells) attr ->
+                   TableFoot attr cells)
+  , property "rows" "footer rows"
+      (pushPandocList pushRow, \(TableFoot _ rows) -> rows)
+      (peekList peekRowFuzzy, \(TableFoot attr _) rows ->
+                                TableFoot attr rows)
+
+  , alias "identifier" "cell ID"         ["attr", "identifier"]
+  , alias "classes"    "cell classes"    ["attr", "classes"]
+  , alias "attributes" "cell attributes" ["attr", "attributes"]
+
+  , method $ defun "clone"
+    ### return
+    <#> parameter peekTableFoot "TableFoot" "self" ""
+    =#> functionResult pushTableFoot "TableFoot" "cloned object"
+  ]
+
+-- | Constructor function for 'Row' values.
+mkTableFoot :: LuaError e => DocumentedFunction e
+mkTableFoot = defun "TableFoot"
+  ### liftPure2 (\mCells mAttr -> TableFoot
+                  (fromMaybe nullAttr mAttr)
+                  (fromMaybe [] mCells))
+  <#> optionalParameter (peekList peekRowFuzzy) "{Row,...}" "rows"
+        "footer rows"
+  <#> optionalParameter peekAttr "Attr" "attr" "table foot attributes"
+  =#> functionResult pushTableFoot "TableFoot" "new TableFoot object"
diff --git a/src/Text/Pandoc/Lua/Marshal/TableHead.hs b/src/Text/Pandoc/Lua/Marshal/TableHead.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Pandoc/Lua/Marshal/TableHead.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE OverloadedStrings    #-}
+{- |
+Copyright               : © 2021 Albert Krewinkel
+SPDX-License-Identifier : MIT
+Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
+
+Marshaling/unmarshaling functions of 'TableHead' values.
+-}
+module Text.Pandoc.Lua.Marshal.TableHead
+  ( peekTableHead
+  , pushTableHead
+  , typeTableHead
+  , mkTableHead
+  ) where
+
+import Control.Applicative (optional)
+import Data.Maybe (fromMaybe)
+import HsLua
+import Text.Pandoc.Lua.Marshal.Attr (peekAttr, pushAttr)
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Lua.Marshal.Row (peekRowFuzzy, pushRow)
+import Text.Pandoc.Definition
+
+-- | Push a TableHead as a userdata value.
+pushTableHead :: LuaError e => TableHead -> LuaE e ()
+pushTableHead = pushUD typeTableHead
+
+-- | Retrieves a 'Cell' from the stack.
+peekTableHead :: LuaError e => Peeker e TableHead
+peekTableHead = peekUD typeTableHead
+
+-- | Row object type.
+typeTableHead :: LuaError e => DocumentedType e TableHead
+typeTableHead = deftype "pandoc TableHead"
+  [ operation Eq $ defun "__eq"
+     ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+     <#> parameter (optional . peekTableHead) "TableHead" "self" ""
+     <#> parameter (optional . peekTableHead) "any" "object" ""
+     =#> functionResult pushBool "boolean" "true iff the two values are equal"
+  , operation Tostring $ lambda
+    ### liftPure show
+    <#> parameter peekTableHead "TableHead" "self" ""
+    =#> functionResult pushString "string" "native Haskell representation"
+  ]
+  [ property "attr" "table head attributes"
+      (pushAttr, \(TableHead attr _) -> attr)
+      (peekAttr, \(TableHead _ cells) attr ->
+                   TableHead attr cells)
+  , property "rows" "header rows"
+      (pushPandocList pushRow, \(TableHead _ rows) -> rows)
+      (peekList peekRowFuzzy, \(TableHead attr _) rows ->
+                                TableHead attr rows)
+
+  , alias "identifier" "cell ID"         ["attr", "identifier"]
+  , alias "classes"    "cell classes"    ["attr", "classes"]
+  , alias "attributes" "cell attributes" ["attr", "attributes"]
+
+  , method $ defun "clone"
+    ### return
+    <#> parameter peekTableHead "TableHead" "self" ""
+    =#> functionResult pushTableHead "TableHead" "cloned object"
+  ]
+
+-- | Constructor function for 'Row' values.
+mkTableHead :: LuaError e => DocumentedFunction e
+mkTableHead = defun "TableHead"
+  ### liftPure2 (\mRows mAttr -> TableHead
+                  (fromMaybe nullAttr mAttr)
+                  (fromMaybe [] mRows))
+  <#> optionalParameter (peekList peekRowFuzzy) "{Row,...}" "rows"
+        "header rows"
+  <#> optionalParameter peekAttr "Attr" "attr" "table head attributes"
+  =#> functionResult pushTableHead "TableHead" "new TableHead object"
diff --git a/src/Text/Pandoc/Lua/Marshal/TableParts.hs b/src/Text/Pandoc/Lua/Marshal/TableParts.hs
--- a/src/Text/Pandoc/Lua/Marshal/TableParts.hs
+++ b/src/Text/Pandoc/Lua/Marshal/TableParts.hs
@@ -14,6 +14,7 @@
   , peekColSpec
   , pushColSpec
   , peekRow
+  , peekRowFuzzy
   , pushRow
   , peekTableBody
   , pushTableBody
@@ -21,6 +22,10 @@
   , pushTableFoot
   , peekTableHead
   , pushTableHead
+    -- * Constructors
+  , mkRow
+  , mkTableFoot
+  , mkTableHead
   ) where
 
 import Control.Applicative (optional)
@@ -30,10 +35,12 @@
 import Text.Pandoc.Lua.Marshal.Attr (peekAttr, pushAttr)
 import {-# SOURCE #-} Text.Pandoc.Lua.Marshal.Block
   ( peekBlocksFuzzy, pushBlocks )
-import Text.Pandoc.Lua.Marshal.Cell (peekCellFuzzy, pushCell)
 import {-# SOURCE #-} Text.Pandoc.Lua.Marshal.Inline
   ( peekInlinesFuzzy, pushInlines )
 import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Lua.Marshal.Row
+import Text.Pandoc.Lua.Marshal.TableFoot
+import Text.Pandoc.Lua.Marshal.TableHead
 import Text.Pandoc.Definition
 
 -- | Push Caption element
@@ -69,17 +76,6 @@
   (ColWidth w)    -> push w
   ColWidthDefault -> pushnil
 
--- | Push a table row as a pair of attr and the list of cells.
-pushRow :: LuaError e => Pusher e Row
-pushRow (Row attr cells) =
-  pushPair pushAttr (pushPandocList pushCell) (attr, cells)
-
--- | Push a table row from a pair of attr and the list of cells.
-peekRow :: LuaError e => Peeker e Row
-peekRow = (uncurry Row <$!>)
-  . retrieving "Row"
-  . peekPair peekAttr (peekList peekCellFuzzy)
-
 -- | Pushes a 'TableBody' value as a Lua table with fields @attr@,
 -- @row_head_columns@, @head@, and @body@.
 pushTableBody :: LuaError e => Pusher e TableBody
@@ -98,32 +94,8 @@
   $ \idx -> TableBody
   <$!> peekFieldRaw peekAttr "attr" idx
   <*>  peekFieldRaw (fmap RowHeadColumns . peekIntegral) "row_head_columns" idx
-  <*>  peekFieldRaw (peekList peekRow) "head" idx
-  <*>  peekFieldRaw (peekList peekRow) "body" idx
-
--- | Push a table head value as the pair of its Attr and rows.
-pushTableHead :: LuaError e => Pusher e TableHead
-pushTableHead (TableHead attr rows) =
-  pushPair pushAttr (pushPandocList pushRow) (attr, rows)
-
--- | Peek a table head value from a pair of Attr and rows.
-peekTableHead :: LuaError e => Peeker e TableHead
-peekTableHead = (uncurry TableHead <$!>)
-  . retrieving "TableHead"
-  . peekPair peekAttr (peekList peekRow)
-
--- | Pushes a 'TableFoot' value as a pair of the Attr value and the list
--- of table rows.
-pushTableFoot :: LuaError e => Pusher e TableFoot
-pushTableFoot (TableFoot attr rows) =
-  pushPair pushAttr (pushPandocList pushRow) (attr, rows)
-
--- | Retrieves a 'TableFoot' value from a pair containing an Attr value
--- and a list of table rows.
-peekTableFoot :: LuaError e => Peeker e TableFoot
-peekTableFoot = (uncurry TableFoot <$!>)
-  . retrieving "TableFoot"
-  . peekPair peekAttr (peekList peekRow)
+  <*>  peekFieldRaw (peekList peekRowFuzzy) "head" idx
+  <*>  peekFieldRaw (peekList peekRowFuzzy) "body" idx
 
 -- | Add a value to the table at the top of the stack at a string-index.
 addField :: LuaError e => Name -> LuaE e () -> LuaE e ()
diff --git a/src/Text/Pandoc/Lua/Topdown.hs b/src/Text/Pandoc/Lua/Topdown.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Pandoc/Lua/Topdown.hs
@@ -0,0 +1,138 @@
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE LambdaCase           #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{- |
+Module      : Text.Pandoc.Lua.Topdown
+Copyright   : © 2012-2021 John MacFarlane,
+              © 2017-2021 Albert Krewinkel
+License     : GNU GPL, version 2 or above
+Maintainer  : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
+
+Walk documents in a filter-suitable way, descending from the root
+towards the leaves.
+-}
+module Text.Pandoc.Lua.Topdown
+  ( TraversalNode (..)
+  , Topdown (..)
+  , TraversalControl (..)
+  )
+where
+
+import Control.Monad ((>=>))
+import Text.Pandoc.Definition
+import Text.Pandoc.Lua.Walk
+import Text.Pandoc.Walk
+
+-- | Helper type to do a preorder traversal of a subtree.
+data TraversalNode
+  = TBlock Block
+  | TBlocks [Block]
+  | TInline Inline
+  | TInlines [Inline]
+
+-- | Type used to traverse a 'Pandoc' AST from top to bottom, i.e.,
+-- processing the root element first and then continue towards the
+-- leaves depth-first. Aborts the descend if 'topdownControl' is 'Stop'.
+data Topdown = Topdown
+  { topdownControl :: TraversalControl
+  , topdownNode :: TraversalNode
+  }
+
+-- | Extracts a list of 'Inline' elements from a 'TraversalNode'.
+-- WARNING: This is a partial function and will throw an error if the
+-- node contains a 'Block' or a list of 'Block's.
+nodeInlines :: TraversalNode -> [Inline]
+nodeInlines = \case
+  TInlines xs -> xs
+  TInline x   -> [x]
+  _            -> error $ "The 'impossible' has happened."
+                       ++ "Please report this as a bug"
+
+-- | Extracts a list of 'Block' elements from a 'TraversalNode'.
+nodeBlocks :: TraversalNode -> [Block]
+nodeBlocks = \case
+  TBlocks xs  -> xs
+  TBlock x    -> [x]
+  TInlines xs -> [Plain xs]
+  TInline x   -> [Plain [x]]
+
+-- | Creates a topdown-walking function for a list of elements.
+walkTopdownM :: (Monad m, Walkable Topdown a)
+             => ([a] -> TraversalNode)
+             -> (a -> TraversalNode)
+             -> (TraversalNode -> [a])
+             -> (Topdown -> m Topdown)
+             -> [a] -> m [a]
+walkTopdownM mkListNode mkElemNode nodeToList f =
+  f . Topdown Continue . mkListNode >=> \case
+    Topdown Stop     node -> return $ nodeToList node
+    Topdown Continue node -> mconcat <$>
+      traverse (f . Topdown Continue . mkElemNode >=> \case
+                   Topdown Stop     node' -> return $ nodeToList node'
+                   Topdown Continue node' -> traverse (walkM f) $
+                                             nodeToList node')
+               (nodeToList node)
+
+-- | Creates a topdown-query function for a list of elements.
+queryTopdown :: (Monoid a, Walkable Topdown b)
+             => ([b] -> TraversalNode)
+             -> (Topdown -> a) -> [b] -> a
+queryTopdown mkListNode f xs =
+  f (Topdown Continue $ mkListNode xs) <> mconcat (map (query f) xs)
+
+instance {-# OVERLAPPING #-} Walkable Topdown [Block] where
+  walkM = walkTopdownM TBlocks TBlock nodeBlocks
+  query = queryTopdown TBlocks
+
+instance {-# OVERLAPPING #-} Walkable Topdown [Inline] where
+  walkM = walkTopdownM TInlines TInline nodeInlines
+  query = queryTopdown TInlines
+
+instance Walkable Topdown Block where
+  walkM = walkBlockM
+  query = queryBlock
+
+instance Walkable Topdown Inline where
+  walkM = walkInlineM
+  query = queryInline
+
+instance Walkable Topdown Pandoc where
+  walkM = walkPandocM
+  query = queryPandoc
+
+instance Walkable Topdown Citation where
+  walkM = walkCitationM
+  query = queryCitation
+
+instance Walkable Topdown Row where
+  walkM = walkRowM
+  query = queryRow
+
+instance Walkable Topdown TableHead where
+  walkM = walkTableHeadM
+  query = queryTableHead
+
+instance Walkable Topdown TableBody where
+  walkM = walkTableBodyM
+  query = queryTableBody
+
+instance Walkable Topdown TableFoot where
+  walkM = walkTableFootM
+  query = queryTableFoot
+
+instance Walkable Topdown Caption where
+  walkM = walkCaptionM
+  query = queryCaption
+
+instance Walkable Topdown Cell where
+  walkM = walkCellM
+  query = queryCell
+
+instance Walkable Topdown MetaValue where
+  walkM = walkMetaValueM
+  query = queryMetaValue
+
+instance Walkable Topdown Meta where
+  walkM f (Meta metamap) = Meta <$> walkM f metamap
+  query f (Meta metamap) = query f metamap
diff --git a/src/Text/Pandoc/Lua/Walk.hs b/src/Text/Pandoc/Lua/Walk.hs
--- a/src/Text/Pandoc/Lua/Walk.hs
+++ b/src/Text/Pandoc/Lua/Walk.hs
@@ -15,10 +15,13 @@
 module Text.Pandoc.Lua.Walk
   ( SpliceList (..)
   , Walkable
+  , TraversalControl (..)
   , walkSplicing
   , walkStraight
   , applyStraight
   , applySplicing
+  , applyStraightFunction
+  , applySplicingFunction
   )
 where
 
@@ -51,7 +54,7 @@
       pure
     Just fn ->
       -- Walk the element with the filter function.
-      walkM $ applyStraightFunction fn pushElement peekElement
+      walkM $ fmap fst . applyStraightFunction fn pushElement peekElement
 
 -- | Applies a filter on an element. The element is pushed to the stack
 -- via the given pusher and calls the filter function with that value,
@@ -64,7 +67,7 @@
     Nothing ->
       -- There is no filter function, do nothing.
       pure x
-    Just fn -> do
+    Just fn -> fst <$>
       -- Apply the function
       applyStraightFunction fn pushElement peekElement x
 
@@ -74,13 +77,15 @@
 -- on the stack.
 applyStraightFunction :: LuaError e
                       => FilterFunction -> Pusher e a -> Peeker e a
-                      -> a -> LuaE e a
+                      -> a -> LuaE e (a, TraversalControl)
 applyStraightFunction fn pushElement peekElement x = do
   pushFilterFunction fn
   pushElement x
-  callWithTraceback 1 1
-  forcePeek . (`lastly` pop 1) $
-    (x <$ peekNil top) <|> peekElement top
+  callWithTraceback 1 2
+  forcePeek . (`lastly` pop 2) $
+    (,)
+    <$> ((x <$ peekNil (nth 2)) <|> peekElement (nth 2))
+    <*> peekTraversalControl top
 
 --
 -- Splicing
@@ -118,7 +123,7 @@
     Nothing ->
       -- There is no filter function, do nothing.
       pure [x]
-    Just fn -> do
+    Just fn -> fst <$>
       -- Apply the function
       applySplicingFunction fn pushElement peekElements x
 
@@ -128,14 +133,17 @@
 -- on the stack.
 applySplicingFunction :: LuaError e
                       => FilterFunction -> Pusher e a -> Peeker e [a]
-                      -> a -> LuaE e [a]
+                      -> a -> LuaE e ([a], TraversalControl)
 applySplicingFunction fn pushElement peekElements x = do
   pushFilterFunction fn
   pushElement x
-  callWithTraceback 1 1
-  forcePeek . (`lastly` pop 1) $ liftLua (ltype top) >>= \case
-    TypeNil -> pure [x]  -- function returned `nil`, keep original value
-    _       -> peekElements top
+  callWithTraceback 1 2
+  forcePeek . (`lastly` pop 2) $
+    (,)
+    <$> (liftLua (ltype (nth 2)) >>= \case
+            TypeNil -> pure [x]  -- function returned `nil`, keep original value
+            _       -> peekElements (nth 2))
+    <*> peekTraversalControl top
 
 --
 -- Helper
@@ -158,3 +166,13 @@
   remove tracebackIdx
   when (result /= OK)
     throwErrorAsException
+
+data TraversalControl = Continue | Stop
+
+-- | Retrieves a Traversal control value: @nil@ or a truthy value
+-- translate to 'Continue', @false@ is treated to mean 'Stop'.
+peekTraversalControl :: Peeker e TraversalControl
+peekTraversalControl idx = (Continue <$ peekNil idx)
+  <|> (liftLua (toboolean top) >>= \case
+          True -> pure Continue
+          False -> pure Stop)
diff --git a/test/test-block.lua b/test/test-block.lua
--- a/test/test-block.lua
+++ b/test/test-block.lua
@@ -246,20 +246,20 @@
     group 'Table' {
       test('access Attr via property `attr`', function ()
         local caption = {long = {Plain 'cap'}}
-        local tbl = Table(caption, {}, {{}, {}}, {}, {{}, {}},
+        local tbl = Table(caption, {}, TableHead(), {}, TableFoot(),
                                  {'my-tbl', {'a'}})
         assert.are_equal(tbl.attr, Attr{'my-tbl', {'a'}})
 
         tbl.attr = Attr{'my-other-tbl', {'b'}}
         assert.are_equal(
-          Table(caption, {}, {{}, {}}, {}, {{}, {}},
+          Table(caption, {}, TableHead(), {}, TableFoot(),
                        {'my-other-tbl', {'b'}}),
           tbl
         )
       end),
       test('access caption via property `caption`', function ()
         local caption = {long = {Plain 'cap'}}
-        local tbl = Table(caption, {}, {{}, {}}, {}, {{}, {}})
+        local tbl = Table(caption, {}, TableHead(), {}, TableFoot())
         assert.are_same(tbl.caption, {long = {Plain 'cap'}})
 
         tbl.caption.short = 'brief'
@@ -270,13 +270,13 @@
           long = {Plain 'extended'}
         }
         assert.are_equal(
-          Table(new_caption, {}, {{}, {}}, {}, {{}, {}}),
+          Table(new_caption, {}, TableHead(), {}, TableFoot()),
           tbl
         )
       end),
       test('access column specifiers via property `colspecs`', function ()
         local colspecs = {{AlignCenter, 1}}
-        local tbl = Table({long = {}}, colspecs, {{}, {}}, {}, {{}, {}})
+        local tbl = Table({long = {}}, colspecs, TableHead(), {}, TableFoot())
         assert.are_same(tbl.colspecs, colspecs)
 
         tbl.colspecs[1][1] = AlignRight
@@ -284,33 +284,39 @@
 
         local new_colspecs = {{AlignRight}}
         assert.are_equal(
-          Table({long = {}}, new_colspecs, {{}, {}}, {}, {{}, {}}),
+          Table({long = {}}, new_colspecs, TableHead(), {}, TableFoot()),
           tbl
         )
       end),
       test('access table head via property `head`', function ()
-        local head = {Attr{'tbl-head'}, {}}
-        local tbl = Table({long = {}}, {}, head, {}, {{}, {}})
+        local head = TableHead({Row{Cell'a'}}, Attr('tbl-head'))
+        local tbl = Table({long = {}}, {}, head, {}, TableFoot())
         assert.are_same(tbl.head, head)
 
-        tbl.head[1] = Attr{'table-head'}
+        local new_head = head:clone()
+        new_head.attr = Attr{'table-head'}
+        new_head.rows = {Row{Cell{'test'}}}
 
-        local new_head = {'table-head', {}}
+        tbl.head = new_head
+
         assert.are_equal(
-          Table({long = {}}, {}, new_head, {}, {{}, {}}),
+          Table({long = {}}, {}, new_head, {}, TableFoot()),
           tbl
         )
       end),
-      test('access table head via property `head`', function ()
-        local foot = {{id = 'tbl-foot'}, {}}
-        local tbl = Table({long = {}}, {}, {{}, {}}, {}, foot)
-        assert.are_same(tbl.foot, {Attr('tbl-foot'), {}})
+      test('access table foot via property `foot`', function ()
+        local foot = TableFoot({Row{Cell{'test'}}}, {id = 'tbl-foot'})
+        local tbl = Table({long = {}}, {}, TableHead(), {}, foot)
+        assert.are_same(tbl.foot, foot)
 
-        tbl.foot[1] = Attr{'table-foot'}
+        local new_foot = foot:clone()
+        new_foot.attr = Attr{'table-foot'}
+        new_foot.rows = {Row{Cell{'test'}}}
 
-        local new_foot = {'table-foot', {}}
+        tbl.foot = new_foot
+
         assert.are_equal(
-          Table({long = {}}, {}, {{}, {}}, {}, new_foot),
+          Table({long = {}}, {}, TableHead(), {}, new_foot),
           tbl
         )
       end)
@@ -448,6 +454,96 @@
       }
       assert.are_same(
         {'Str', 'Inlines', 'Para', 'CodeBlock', 'Blocks'},
+        names
+      )
+    end),
+    test('topdown traversal works', function ()
+      local names = List{}
+      local tbl = Table(
+        {long = {}},
+        {{AlignCenter, 1}},
+        TableHead{Row({Cell{'test', Para{'foo', Emph{'bar'}}}}, 'foo')},
+        {},
+        TableFoot()
+      )
+      tbl:walk{
+        traverse = 'topdown',
+        Blocks = function (_)
+          names:insert('Blocks')
+        end,
+        Block = function (b)
+          names:insert(b.t)
+        end,
+        Inline = function (i)
+          names:insert(i.t)
+        end,
+        Inlines = function (_)
+          names:insert('Inlines')
+        end,
+      }
+      assert.are_same(
+        -- Caption  Cell
+        {'Blocks', 'Blocks',
+         'Plain', 'Inlines', 'Str',
+         'Para', 'Inlines', 'Str',
+         'Emph', 'Inlines', 'Str'
+        },
+        names
+      )
+    end),
+    test('truncating topdown traversal works', function ()
+      local names = List{}
+      local div = Div{
+        Para{Emph 'a'},
+        Plain{'b'},
+        CodeBlock('c')
+      }
+      local filter
+      filter = {
+        traverse = 'topdown',
+        Block = function (b)
+          names:insert(b.t)
+          if b.t == 'Para' then
+            return b, false
+          end
+        end,
+        Inline = function (i)
+          names:insert(i.t)
+          return i:walk(filter), false  -- continue 'manually'
+        end,
+      }
+      div:walk(filter)
+      assert.are_same(
+        {'Para',  -- Emph is skipped!
+         'Plain', 'Str',
+         'CodeBlock',
+        },
+        names
+      )
+    end),
+    test('truncating topdown traversal works in inlines', function ()
+      local names = List{}
+      local div = Div{
+        Para{Emph 'a'},
+        Plain{'b'},
+      }
+      div:walk {
+        traverse = 'topdown',
+        Block = function (b)
+          names:insert(b.t)
+          if b.t == 'Plain' then
+            return nil, false
+          end
+        end,
+        Emph = function (i)
+          names:insert(i.t)
+          return nil, false
+        end,
+      }
+      assert.are_same(
+        {'Para', 'Emph', -- Str is skipped
+         'Plain',        -- Str is skipped here, too
+        },
         names
       )
     end),
diff --git a/test/test-inline.lua b/test/test-inline.lua
--- a/test/test-inline.lua
+++ b/test/test-inline.lua
@@ -412,7 +412,6 @@
           names:insert('Inlines')
         end,
       }
-      print(names)
       assert.are_equal(
         'Str, Space, Str, Space, Str, Inlines, Para, CodeBlock, Blocks',
         table.concat(names, ', ')
diff --git a/test/test-list.lua b/test/test-list.lua
--- a/test/test-list.lua
+++ b/test/test-list.lua
@@ -73,11 +73,19 @@
         local list = List:new {19, 23, 29, 71}
         assert.are_equal(23, list:find(23, 1))
         assert.are_equal(23, list:find(23, 2))
+        assert.are_equal(23, list:find(23, -4))
         assert.is_nil(list:find(23, 3))
+        assert.is_nil(list:find(23, -2))
       end),
       test('returns nil if element not found', function ()
         assert.is_nil((List:new {18, 20, 22, 0, 24}):find('0'))
       end),
+      test('fails if start index is not an integer', function ()
+        assert.error_matches(
+          function () List:new{}:find(0, 'NaN') end,
+          'number expected, got string'
+        )
+      end)
     },
 
     group 'find_if' {
@@ -99,6 +107,17 @@
         local is_zero = function (n) return n == 0 end
         assert.is_nil((List:new {18, 20, 22, 24, 27}):find_if(is_zero))
       end),
+      test('respects start index', function ()
+        local list = List:new {9, 29, 3, 71}
+        assert.are_equal(71, list:find_if(function(n) return n > 10 end, 3))
+        assert.are_equal(29, list:find_if(function(n) return n > 10 end, -3))
+      end),
+      test('fails if start index is not an integer', function ()
+        assert.error_matches(
+          function () List:new{}:find(0, 'NaN') end,
+          'number expected, got string'
+        )
+      end)
     },
 
     group 'includes' {
@@ -142,6 +161,14 @@
         local primes = List:new {2, 3, 5, 7}
         local indices = primes:map(function (x, i) return i end)
         assert.are_same(List{1, 2, 3, 4}, indices)
+      end),
+      test('map returns a generic list', function ()
+        local custom = CustomList{'α', 'β'}
+        assert.are_equal(debug.getmetatable(custom).__name, 'CustomList')
+        assert.are_same(
+          debug.getmetatable(custom:map(tostring)).__name,
+          'List'
+        )
       end)
     },
 
@@ -194,22 +221,54 @@
   },
   group 'Operations' {
     group 'concatenation' {
-    test('yields a concatenated list', function ()
-      assert.are_same(List {3, 4, 5, 6}, List{3, 4} .. List {5, 6})
-    end),
-    test('does not modify its operands', function ()
-      local a = List {54, 74}
-      local b = List {90, 2014}
-      local result = a .. b
-      assert.are_same(a, List{54, 74})
-      assert.are_same(b, List{90, 2014})
-    end),
-    test('sets metatable of first operand on result', function ()
-      local result = {1, 4} .. List{9, 16}
-      assert.are_equal(nil, getmetatable(result))
-      result = List{1, 4} .. {9, 16}
-      assert.are_equal(List, getmetatable(result))
-    end),
+      test('yields a concatenated list', function ()
+        assert.are_same(List {3, 4, 5, 6}, List{3, 4} .. List {5, 6})
+      end),
+      test('does not modify its operands', function ()
+        local a = List {54, 74}
+        local b = List {90, 2014}
+        local result = a .. b
+        assert.are_same(a, List{54, 74})
+        assert.are_same(b, List{90, 2014})
+      end),
+      test('sets metatable of first operand on result', function ()
+        local result = {1, 4} .. List{9, 16}
+        assert.are_equal(nil, getmetatable(result))
+        result = List{1, 4} .. {9, 16}
+        assert.are_equal(List, getmetatable(result))
+      end),
+    },
+    group 'equality' {
+      test('lists are equal if all elements are equal', function ()
+        assert.are_equal(
+          List {5, 6, 7, 8},
+          List {5, 6, 7, 8}
+        )
+      end),
+      test('lists are not equal if their metatables are different', function ()
+        assert.is_truthy(
+          List {18, 20, 2, 0, 24} ~=
+          CustomList {18, 20, 2, 0, 24}
+        )
+      end),
+      test('lists are not equal if one is a plain table', function ()
+        assert.is_truthy(
+          List {18, 20, 2, 0, 24} ~=
+          {18, 20, 2, 0, 24}
+        )
+      end),
+      test('lists are not equal if an element differs', function ()
+        assert.is_truthy(
+          List {18, 20, 22, 23, 24} ~=
+          List {18, 20, 22, 0, 24}
+        )
+      end),
+      test('can compare to a string', function ()
+        assert.is_truthy(
+          List {'a', 'b', 'c'} ~=
+          "abc"
+        )
+      end),
     }
   },
 }
diff --git a/test/test-metavalue.lua b/test/test-metavalue.lua
--- a/test/test-metavalue.lua
+++ b/test/test-metavalue.lua
@@ -11,5 +11,9 @@
       assert.are_equal(type(metalist.insert), 'function')
       assert.are_equal(type(metalist.remove), 'function')
     end),
+    test('Numbers are treated as strings', function ()
+      local metalist = MetaList{5, 23, 13.37}
+      assert.are_same(metalist, MetaList{'5', '23', '13.37'})
+    end)
   }
 }
diff --git a/test/test-pandoc-lua-marshal.hs b/test/test-pandoc-lua-marshal.hs
--- a/test/test-pandoc-lua-marshal.hs
+++ b/test/test-pandoc-lua-marshal.hs
@@ -28,6 +28,13 @@
   listTests <- run @Lua.Exception $ do
     openlibs
     pushListModule *> setglobal "List"
+    -- Create a custom List type with constructor "CustomList"
+    pushHaskellFunction $ do
+      settop 1
+      newListMetatable "CustomList" (pure ())
+      setmetatable (nthBottom 1)
+      return 1
+    setglobal "CustomList"
     translateResultsFromFile "test/test-list.lua"
 
   listAttributeTests <- run @Lua.Exception $ do
@@ -53,52 +60,19 @@
     translateResultsFromFile "test/test-citation.lua"
 
   inlineTests <- run @Lua.Exception $ do
-    openlibs
-    pushListModule *> setglobal "List"
-    register' mkAttr
-    register' mkCitation
-    register' mkInlines
-    registerConstants (Proxy @CitationMode)
-    registerConstants (Proxy @MathType)
-    registerConstants (Proxy @QuoteType)
-    forM_ inlineConstructors register'
-    forM_ blockConstructors register'
+    registerDefault
     translateResultsFromFile "test/test-inline.lua"
 
   blockTests <- run @Lua.Exception $ do
-    openlibs
-    pushListModule *> setglobal "List"
-    register' mkAttr
-    register' mkBlocks
-    register' mkListAttributes
-    registerConstants (Proxy @Alignment)
-    registerConstants (Proxy @ListNumberStyle)
-    registerConstants (Proxy @ListNumberStyle)
-    forM_ inlineConstructors register'
-    forM_ blockConstructors register'
+    registerDefault
     translateResultsFromFile "test/test-block.lua"
 
   cellTests <- run @Lua.Exception $ do
-    openlibs
-    pushListModule *> setglobal "List"
-    register' mkAttr
-    register' mkBlocks
-    register' mkCell
-    register' mkListAttributes
-    registerConstants (Proxy @Alignment)
-    forM_ inlineConstructors register'
-    forM_ blockConstructors register'
+    registerDefault
     translateResultsFromFile "test/test-cell.lua"
 
   simpleTableTests <- run @Lua.Exception $ do
-    openlibs
-    pushListModule *> setglobal "List"
-    register' mkAttr
-    register' mkListAttributes
-    register' mkSimpleTable
-    registerConstants (Proxy @Alignment)
-    forM_ inlineConstructors register'
-    forM_ blockConstructors register'
+    registerDefault
     translateResultsFromFile "test/test-simpletable.lua"
 
   metavalueTests <- run @Lua.Exception $ do
@@ -108,12 +82,7 @@
     translateResultsFromFile "test/test-metavalue.lua"
 
   pandocTests <- run @Lua.Exception $ do
-    openlibs
-    pushListModule *> setglobal "List"
-    register' mkMeta
-    register' mkPandoc
-    forM_ inlineConstructors register'
-    forM_ blockConstructors register'
+    registerDefault
     translateResultsFromFile "test/test-pandoc.lua"
 
   defaultMain $ testGroup "pandoc-lua-marshal"
@@ -129,6 +98,31 @@
     , metavalueTests
     , pandocTests
     ]
+
+-- | Registers all constructors and string constants in the global
+-- environment.
+registerDefault :: LuaError e => LuaE e ()
+registerDefault = do
+  openlibs
+  pushListModule *> setglobal "List"
+  register' mkAttr
+  register' mkBlocks
+  register' mkCell
+  register' mkCitation
+  register' mkInlines
+  register' mkListAttributes
+  register' mkPandoc
+  register' mkRow
+  register' mkSimpleTable
+  register' mkTableHead
+  register' mkTableFoot
+  registerConstants (Proxy @Alignment)
+  registerConstants (Proxy @ListNumberStyle)
+  registerConstants (Proxy @ListNumberStyle)
+  registerConstants (Proxy @MathType)
+  registerConstants (Proxy @QuoteType)
+  forM_ inlineConstructors register'
+  forM_ blockConstructors register'
 
 register' :: LuaError e => DocumentedFunction e -> LuaE e ()
 register' f = do
diff --git a/test/test-pandoc.lua b/test/test-pandoc.lua
--- a/test/test-pandoc.lua
+++ b/test/test-pandoc.lua
@@ -54,5 +54,88 @@
         }
       )
     end),
+    test('default traversal is typewise, bottom-up', function ()
+      local names = List{}
+      local doc = Pandoc(
+        Blocks{
+          Div{
+            Plain{Emph 'a'},
+            Para{'b'},
+            CodeBlock('c')
+          }
+        },
+        { test = Blocks 'foo' }
+      )
+      doc:walk {
+        Block = function (b)
+          names:insert(b.t)
+        end,
+        Inline = function (i)
+          names:insert(i.t)
+        end,
+        Pandoc = function (_)
+          names:insert('Pandoc')
+        end,
+        Meta = function (_)
+          names:insert('Meta')
+        end
+      }
+      assert.are_same(
+        { 'Str',   -- in meta value
+          'Str',   -- in Emph
+          'Emph',
+          'Str',   -- in Para,
+          'Plain', -- in meta value
+          'Plain',
+          'Para',
+          'CodeBlock',
+          'Div',
+          'Meta',
+          'Pandoc'
+        },
+        names
+      )
+    end),
+    test('truncating topdown traversal works', function ()
+      local names = List{}
+      local doc = Pandoc(
+        Blocks{
+          Div{
+            Plain{Emph 'a'},
+            Para{'b'},
+            CodeBlock('c')
+          }
+        },
+        { test = Blocks 'foo' }
+      )
+      doc:walk {
+        traverse = 'topdown',
+        Block = function (b)
+          names:insert(b.t)
+          if b.t == 'Para' then
+            return b, false
+          end
+        end,
+        Inline = function (i)
+          names:insert(i.t)
+        end,
+        Pandoc = function (_)
+          names:insert('Pandoc')
+        end,
+        Meta = function (_)
+          names:insert('Meta')
+        end
+      }
+      assert.are_same(
+        { 'Pandoc',
+          'Meta', 'Plain', 'Str', -- Meta and meta value
+          'Div',
+          'Plain', 'Emph', 'Str',
+          'Para',                 -- Str is skipped!
+          'CodeBlock'
+        },
+        names
+      )
+    end),
   }
 }
