diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,19 @@
 
 `pandoc-lua-marshal` uses [PVP Versioning][].
 
+## 0.3.2
+
+Released 2026-01-08.
+
+  * Added a docstring to the `Pandoc.normalize` method.
+
+  * Added `TableBody` constructor. (Sean Soon)
+
+  * Fixed comments in files `TableFoot.hs` and `TableHead.hs`
+    (Sean Soon).
+
+  * Allow hslua-2.5 and containers-0.8.\*.
+
 ## 0.3.1
 
 Released 2025-06-23.
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2021-2025 Albert Krewinkel
+Copyright (c) 2021-2026 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
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.3.1
+version:             0.3.2
 synopsis:            Use pandoc types in Lua
 description:         This package provides functions to marshal and unmarshal
                      pandoc document types to and from Lua.
@@ -19,17 +19,12 @@
 license-file:        LICENSE
 author:              Albert Krewinkel, John MacFarlane
 maintainer:          Albert Krewinkel <albert@zeitkraut.de>
-copyright:           © 2017-2025 Albert Krewinkel, John MacFarlane
+copyright:           © 2017-2026 Albert Krewinkel, John MacFarlane
 category:            Foreign
 build-type:          Simple
 extra-doc-files:     README.md
                    , CHANGELOG.md
-tested-with:         GHC == 8.8.4
-                   , GHC == 8.10.7
-                   , GHC == 9.0.2
-                   , GHC == 9.2.5
-                   , GHC == 9.4.4
-                   , GHC == 9.6
+tested-with:         GHC == 9.6
                    , GHC == 9.8
                    , GHC == 9.10
                    , GHC == 9.12
@@ -51,11 +46,11 @@
   build-depends:       base                  >= 4.12     && < 5
                      , aeson                 >= 1.5      && < 2.3
                      , bytestring            >= 0.10     && < 0.13
-                     , containers            >= 0.6      && < 0.8
+                     , containers            >= 0.6      && < 0.9
                      , exceptions            >= 0.8      && < 0.11
-                     , hslua                 >= 2.2      && < 2.5
+                     , hslua                 >= 2.2      && < 2.6
                      , hslua-list            >= 1.1.4    && < 1.2
-                     , hslua-marshalling     >= 2.2      && < 2.4
+                     , hslua-marshalling     >= 2.3      && < 2.4
                      , pandoc-types          >= 1.23     && < 1.24
                      , safe                  >= 0.3      && < 0.4
                      , text                  >= 1.1.1.0  && < 1.3
@@ -66,12 +61,11 @@
                        -Widentities
                        -Wincomplete-record-updates
                        -Wincomplete-uni-patterns
+                       -Wmissing-deriving-strategies
                        -Wmissing-export-lists
                        -Wpartial-fields
                        -Wredundant-constraints
                        -fhide-source-paths
-  if impl(ghc >= 8.8)
-    ghc-options:       -Wmissing-deriving-strategies
 
   default-language:    Haskell2010
 
@@ -98,6 +92,7 @@
                      , Text.Pandoc.Lua.Marshal.QuoteType
                      , Text.Pandoc.Lua.Marshal.Row
                      , Text.Pandoc.Lua.Marshal.SimpleTable
+                     , Text.Pandoc.Lua.Marshal.TableBody
                      , Text.Pandoc.Lua.Marshal.TableFoot
                      , Text.Pandoc.Lua.Marshal.TableHead
                      , Text.Pandoc.Lua.Marshal.TableParts
diff --git a/src/Text/Pandoc/Lua/Marshal/AST.hs b/src/Text/Pandoc/Lua/Marshal/AST.hs
--- a/src/Text/Pandoc/Lua/Marshal/AST.hs
+++ b/src/Text/Pandoc/Lua/Marshal/AST.hs
@@ -1,5 +1,5 @@
 {- |
-Copyright  : © 2021-2025 Albert Krewinkel
+Copyright  : © 2021-2026 Albert Krewinkel
 License    : MIT
 Maintainer : Albert Krewinkel <albert@zeitkraut.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/Alignment.hs b/src/Text/Pandoc/Lua/Marshal/Alignment.hs
--- a/src/Text/Pandoc/Lua/Marshal/Alignment.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Alignment.hs
@@ -1,5 +1,5 @@
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
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
@@ -49,7 +49,7 @@
 import Text.Pandoc.Lua.Marshal.Shared (walkBlocksAndInlines)
 import Text.Pandoc.Lua.Marshal.TableParts
   ( peekColSpec, pushColSpec
-  , peekTableBody, pushTableBody
+  , peekTableBodyFuzzy, pushTableBody
   , peekTableFoot, pushTableFoot
   , peekTableHead, pushTableHead
   )
@@ -188,7 +188,7 @@
       (pushPandocList pushTableBody, \case
           Table _ _ _ _ bs _ -> Actual bs
           _                  -> Absent)
-      (peekList peekTableBody, \case
+      (peekList peekTableBodyFuzzy, \case
           Table attr c cs h _ f -> Actual . (\bs -> Table attr c cs h bs f)
           _                     -> const Absent)
   , possibleProperty "caption" "element caption"
@@ -455,7 +455,7 @@
     <#> parameter (peekList peekColSpec) "{ColSpec,...}" "colspecs"
                   "column alignments and widths"
     <#> parameter peekTableHead "TableHead" "head" "table head"
-    <#> parameter (peekList peekTableBody) "{TableBody,...}" "bodies"
+    <#> parameter (peekList peekTableBodyFuzzy) "{TableBody,...}" "bodies"
                   "table bodies"
     <#> parameter peekTableFoot "TableFoot" "foot" "table foot"
     <#> optAttrParam
diff --git a/src/Text/Pandoc/Lua/Marshal/Caption.hs b/src/Text/Pandoc/Lua/Marshal/Caption.hs
--- a/src/Text/Pandoc/Lua/Marshal/Caption.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Caption.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/Cell.hs b/src/Text/Pandoc/Lua/Marshal/Cell.hs
--- a/src/Text/Pandoc/Lua/Marshal/Cell.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Cell.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {-# LANGUAGE LambdaCase           #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
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
@@ -1,6 +1,6 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/CitationMode.hs b/src/Text/Pandoc/Lua/Marshal/CitationMode.hs
--- a/src/Text/Pandoc/Lua/Marshal/CitationMode.hs
+++ b/src/Text/Pandoc/Lua/Marshal/CitationMode.hs
@@ -1,5 +1,5 @@
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/Content.hs b/src/Text/Pandoc/Lua/Marshal/Content.hs
--- a/src/Text/Pandoc/Lua/Marshal/Content.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Content.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE LambdaCase        #-}
 {-# LANGUAGE OverloadedStrings #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
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
@@ -3,7 +3,7 @@
 {-# LANGUAGE TypeApplications     #-}
 {-# LANGUAGE ScopedTypeVariables  #-}
 {- |
-Copyright  : © 2021-2025 Albert Krewinkel
+Copyright  : © 2021-2026 Albert Krewinkel
 License    : MIT
 Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/Format.hs b/src/Text/Pandoc/Lua/Marshal/Format.hs
--- a/src/Text/Pandoc/Lua/Marshal/Format.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Format.hs
@@ -1,5 +1,5 @@
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/List.hs b/src/Text/Pandoc/Lua/Marshal/List.hs
--- a/src/Text/Pandoc/Lua/Marshal/List.hs
+++ b/src/Text/Pandoc/Lua/Marshal/List.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE LambdaCase        #-}
 {-# LANGUAGE OverloadedStrings #-}
 {- |
-Copyright  : © 2021-2025 Albert Krewinkel
+Copyright  : © 2021-2026 Albert Krewinkel
 License    : MIT
 Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
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
@@ -1,7 +1,7 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {-# LANGUAGE TupleSections        #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/MathType.hs b/src/Text/Pandoc/Lua/Marshal/MathType.hs
--- a/src/Text/Pandoc/Lua/Marshal/MathType.hs
+++ b/src/Text/Pandoc/Lua/Marshal/MathType.hs
@@ -1,5 +1,5 @@
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
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
@@ -3,7 +3,7 @@
 {-# LANGUAGE ScopedTypeVariables  #-}
 {-# LANGUAGE TypeApplications     #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
@@ -55,7 +55,7 @@
       (MetaInlines . (:[]) <$!> peekInline idx) <|>
       (MetaBlocks . (:[]) <$!> peekBlock idx)
 
-    TypeTable   -> optional (getName idx) >>= \case
+    TypeTable   -> optional (getName' idx) >>= \case
       Just "Inlines" -> MetaInlines <$!> peekInlinesFuzzy idx
       Just "Blocks"  -> MetaBlocks  <$!> peekBlocksFuzzy idx
       Just "List"    -> MetaList <$!> peekList peekMetaValue idx
@@ -71,7 +71,7 @@
     _ -> failPeek "could not get meta value"
 
  where
-  getName idx = liftLua (getmetafield idx "__name") >>= \case
+  getName' idx = liftLua (getmetafield idx "__name") >>= \case
     TypeNil -> failPeek "no name"
     _ -> peekName idx `lastly` pop 1
 
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
@@ -3,7 +3,7 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {-# LANGUAGE TypeApplications     #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
@@ -83,6 +83,9 @@
       ### liftPure normalize
       <#> udparam typePandoc "self" ""
       =#> udresult typePandoc "cloned and normalized document"
+      #? "Perform a normalization of Pandoc documents. E.g., multiple\
+          \ successive spaces are collapsed, and tables are normalized, so\
+          \ that all rows and columns contain the same number of cells."
 
   , method $ defun "walk"
     ### flip applyFully
diff --git a/src/Text/Pandoc/Lua/Marshal/QuoteType.hs b/src/Text/Pandoc/Lua/Marshal/QuoteType.hs
--- a/src/Text/Pandoc/Lua/Marshal/QuoteType.hs
+++ b/src/Text/Pandoc/Lua/Marshal/QuoteType.hs
@@ -1,5 +1,5 @@
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/Row.hs b/src/Text/Pandoc/Lua/Marshal/Row.hs
--- a/src/Text/Pandoc/Lua/Marshal/Row.hs
+++ b/src/Text/Pandoc/Lua/Marshal/Row.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {-# LANGUAGE LambdaCase           #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
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,7 +1,7 @@
 {-# LANGUAGE FlexibleContexts     #-}
 {-# LANGUAGE OverloadedStrings    #-}
 {- |
-Copyright   : © 2021-2025 Albert Krewinkel
+Copyright   : © 2021-2026 Albert Krewinkel
 License     : MIT
 Maintainer  : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
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
@@ -2,7 +2,7 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {-# LANGUAGE ScopedTypeVariables  #-}
 {- |
-Copyright   : © 2021-2025 Albert Krewinkel
+Copyright   : © 2021-2026 Albert Krewinkel
 License     : MIT
 Maintainer  : Albert Krewinkel <albert@zeitkraut.de>
 
diff --git a/src/Text/Pandoc/Lua/Marshal/TableBody.hs b/src/Text/Pandoc/Lua/Marshal/TableBody.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Pandoc/Lua/Marshal/TableBody.hs
@@ -0,0 +1,109 @@
+{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE LambdaCase           #-}
+{- |
+Copyright               : © 2021-2026 Albert Krewinkel,
+                          © 2025-2026 Sean Soon
+SPDX-License-Identifier : MIT
+Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
+
+Marshaling/unmarshaling functions of 'TableBody' values.
+-}
+module Text.Pandoc.Lua.Marshal.TableBody
+  ( peekTableBody
+  , peekTableBodyFuzzy
+  , pushTableBody
+  , typeTableBody
+  , mkTableBody
+  ) where
+
+import Control.Applicative (optional)
+import Data.Aeson (encode)
+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 TableBody as a userdata value.
+pushTableBody :: LuaError e => TableBody -> LuaE e ()
+pushTableBody = pushUD typeTableBody
+
+-- | Retrieves a TableBody from the stack.
+peekTableBody :: LuaError e => Peeker e TableBody
+peekTableBody = peekUD typeTableBody
+
+-- | Retrieves a TableBody from the stack, accepting either a 
+-- 'pandoc TableBody' userdata object or a table with fields @attr@,
+-- @body@, @head@, @row_head_columns@.
+peekTableBodyFuzzy :: LuaError e => Peeker e TableBody
+peekTableBodyFuzzy idx = retrieving "TableBody" $ liftLua (ltype idx) >>= \case
+  TypeUserdata -> peekTableBody idx
+  TypeTable -> do
+    attr <- peekFieldRaw peekAttr "attr" idx
+    body <- peekFieldRaw (peekList peekRowFuzzy) "body" idx
+    ihead <- peekFieldRaw (peekList peekRowFuzzy) "head" idx
+    rhc <- peekFieldRaw (fmap RowHeadColumns . peekIntegral) "row_head_columns" idx
+    return $! TableBody attr rhc ihead body
+  _ -> failPeek =<< typeMismatchMessage "Cell or table" idx
+
+-- | TableBody object type.
+typeTableBody :: LuaError e => DocumentedType e TableBody
+typeTableBody = deftype "TableBody"
+  [ operation Eq $ defun "__eq"
+     ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))
+     <#> parameter (optional . peekTableBody) "TableBody" "self" ""
+     <#> parameter (optional . peekTableBody) "any" "object" ""
+     =#> functionResult pushBool "boolean" "true iff the two values are equal"
+  , operation Tostring $ lambda
+    ### liftPure show
+    <#> parameter peekTableBody "TableBody" "self" ""
+    =#> functionResult pushString "string" "native Haskell representation"
+  , operation (CustomOperation "__tojson") $ lambda
+    ### liftPure encode
+    <#> udparam typeTableBody "self" ""
+    =#> functionResult pushLazyByteString "string" "JSON representation"
+  ]
+  [ property "attr" "table body attributes"
+      (pushAttr, \(TableBody attr _ _ _) -> attr)
+      (peekAttr, \(TableBody _ body ihead rhc) attr ->
+                   TableBody attr body ihead rhc)
+  , property "row_head_columns" "number of columns taken up by the row head of each row of the TableBody"
+    (pushIntegral, \(TableBody _ (RowHeadColumns rhc) _ _) -> rhc)
+    (peekIntegral, \(TableBody attr _ ihead body) rhc ->
+                      TableBody attr (RowHeadColumns rhc) ihead body)
+  , property "head" "intermediate head"
+      (pushPandocList pushRow, \(TableBody _ _ ihead _) -> ihead)
+      (peekList peekRowFuzzy, \(TableBody attr rhc _ body) ihead ->
+                                TableBody attr rhc ihead body)
+  , property "body" "table body rows"
+      (pushPandocList pushRow, \(TableBody _ _ _ body) -> body)
+      (peekList peekRowFuzzy, \(TableBody attr rhc ihead _) body ->
+                                TableBody attr rhc ihead body)
+  , alias "identifier" "cell ID"         ["attr", "identifier"]
+  , alias "classes"    "cell classes"    ["attr", "classes"]
+  , alias "attributes" "cell attributes" ["attr", "attributes"]
+
+  , method $ defun "clone"
+    ### return
+    <#> parameter peekTableBody "TableBody" "self" ""
+    =#> functionResult pushTableBody "TableBody" "cloned object"
+  ]
+
+-- | Constructor function for 'TableBody' values.
+mkTableBody :: LuaError e => DocumentedFunction e
+mkTableBody = defun "TableBody"
+  ### liftPure4 (\mBody mHead mRhc mAttr -> TableBody
+                  (fromMaybe nullAttr mAttr)
+                  (maybe 0 RowHeadColumns mRhc)
+                  (fromMaybe [] mHead)
+                  (fromMaybe [] mBody))
+  <#> opt (parameter (peekList peekRowFuzzy) "{Row,...}" "body"
+           "list of table rows")
+  <#> opt (parameter (peekList peekRowFuzzy) "{Row,...}" "head"
+           "intermediate head")
+  <#> opt (parameter peekIntegral "integer" "row_head_columns"
+           "number of columns taken up by the row head of each row of the TableBody")
+  <#> opt (parameter peekAttr "Attr" "attr" "table body attributes")
+  =#> functionResult pushTableBody "TableBody" "new TableBody object"
+  #? "Creates a table body."
diff --git a/src/Text/Pandoc/Lua/Marshal/TableFoot.hs b/src/Text/Pandoc/Lua/Marshal/TableFoot.hs
--- a/src/Text/Pandoc/Lua/Marshal/TableFoot.hs
+++ b/src/Text/Pandoc/Lua/Marshal/TableFoot.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
@@ -26,11 +26,11 @@
 pushTableFoot :: LuaError e => TableFoot -> LuaE e ()
 pushTableFoot = pushUD typeTableFoot
 
--- | Retrieves a 'Cell' from the stack.
+-- | Retrieves a TableFoot from the stack.
 peekTableFoot :: LuaError e => Peeker e TableFoot
 peekTableFoot = peekUD typeTableFoot
 
--- | Row object type.
+-- | TableFoot object type.
 typeTableFoot :: LuaError e => DocumentedType e TableFoot
 typeTableFoot = deftype "TableFoot"
   [ operation Eq $ defun "__eq"
@@ -66,7 +66,7 @@
     =#> functionResult pushTableFoot "TableFoot" "cloned object"
   ]
 
--- | Constructor function for 'Row' values.
+-- | Constructor function for TableFoot values.
 mkTableFoot :: LuaError e => DocumentedFunction e
 mkTableFoot = defun "TableFoot"
   ### liftPure2 (\mCells mAttr -> TableFoot
diff --git a/src/Text/Pandoc/Lua/Marshal/TableHead.hs b/src/Text/Pandoc/Lua/Marshal/TableHead.hs
--- a/src/Text/Pandoc/Lua/Marshal/TableHead.hs
+++ b/src/Text/Pandoc/Lua/Marshal/TableHead.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE OverloadedStrings    #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
@@ -26,11 +26,11 @@
 pushTableHead :: LuaError e => TableHead -> LuaE e ()
 pushTableHead = pushUD typeTableHead
 
--- | Retrieves a 'Cell' from the stack.
+-- | Retrieves a TableHead from the stack.
 peekTableHead :: LuaError e => Peeker e TableHead
 peekTableHead = peekUD typeTableHead
 
--- | Row object type.
+-- | TableHead object type.
 typeTableHead :: LuaError e => DocumentedType e TableHead
 typeTableHead = deftype "TableHead"
   [ operation Eq $ defun "__eq"
@@ -66,7 +66,7 @@
     =#> functionResult pushTableHead "TableHead" "cloned object"
   ]
 
--- | Constructor function for 'Row' values.
+-- | Constructor function for TableHead values.
 mkTableHead :: LuaError e => DocumentedFunction e
 mkTableHead = defun "TableHead"
   ### liftPure2 (\mRows mAttr -> TableHead
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
@@ -1,7 +1,7 @@
 {-# LANGUAGE LambdaCase           #-}
 {-# LANGUAGE OverloadedStrings    #-}
 {- |
-Copyright               : © 2021-2025 Albert Krewinkel
+Copyright               : © 2021-2026 Albert Krewinkel
 SPDX-License-Identifier : MIT
 Maintainer              : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
 
@@ -15,6 +15,7 @@
   , peekRowFuzzy
   , pushRow
   , peekTableBody
+  , peekTableBodyFuzzy
   , pushTableBody
   , peekTableFoot
   , pushTableFoot
@@ -22,6 +23,7 @@
   , pushTableHead
     -- * Constructors
   , mkRow
+  , mkTableBody
   , mkTableFoot
   , mkTableHead
   ) where
@@ -30,9 +32,8 @@
 import Control.Monad ((<$!>))
 import HsLua
 import Text.Pandoc.Lua.Marshal.Alignment (peekAlignment, pushAlignment)
-import Text.Pandoc.Lua.Marshal.Attr (peekAttr, pushAttr)
-import Text.Pandoc.Lua.Marshal.List (pushPandocList)
 import Text.Pandoc.Lua.Marshal.Row
+import Text.Pandoc.Lua.Marshal.TableBody
 import Text.Pandoc.Lua.Marshal.TableFoot
 import Text.Pandoc.Lua.Marshal.TableHead
 import Text.Pandoc.Definition
@@ -55,31 +56,3 @@
 pushColWidth = \case
   (ColWidth w)    -> push w
   ColWidthDefault -> pushnil
-
--- | Pushes a 'TableBody' value as a Lua table with fields @attr@,
--- @row_head_columns@, @head@, and @body@.
-pushTableBody :: LuaError e => Pusher e TableBody
-pushTableBody (TableBody attr (RowHeadColumns rowHeadColumns) head' body) = do
-    newtable
-    addField "attr" (pushAttr attr)
-    addField "row_head_columns" (pushIntegral rowHeadColumns)
-    addField "head" (pushPandocList pushRow head')
-    addField "body" (pushPandocList pushRow body)
-
--- | Retrieves a 'TableBody' value from a Lua table with fields @attr@,
--- @row_head_columns@, @head@, and @body@.
-peekTableBody :: LuaError e => Peeker e TableBody
-peekTableBody = fmap (retrieving "TableBody")
-  . typeChecked "table" istable
-  $ \idx -> TableBody
-  <$!> peekFieldRaw peekAttr "attr" idx
-  <*>  peekFieldRaw (fmap RowHeadColumns . peekIntegral) "row_head_columns" idx
-  <*>  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 ()
-addField key pushFieldValue = do
-  pushName key
-  pushFieldValue
-  rawset (nth 3)
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
@@ -74,6 +74,10 @@
     registerDefault
     translateResultsFromFile "test/test-pandoc.lua"
 
+  tableTests <- run @Lua.Exception $ do
+    registerDefault
+    translateResultsFromFile "test/test-table.lua"
+
   defaultMain $ testGroup "pandoc-lua-marshal"
     [ roundtrips
     , listAttributeTests
@@ -85,6 +89,7 @@
     , simpleTableTests
     , metavalueTests
     , pandocTests
+    , tableTests
     ]
 
 -- | Registers all constructors and string constants in the global
@@ -102,6 +107,7 @@
   register' mkPandoc
   register' mkRow
   register' mkSimpleTable
+  register' mkTableBody
   register' mkTableHead
   register' mkTableFoot
   registerConstants (Proxy @Alignment)
@@ -182,7 +188,7 @@
     ioProperty . roundtripEqual pushQuoteType peekQuoteType
 
   , testProperty "TableBody" $
-    ioProperty . roundtripEqual pushTableBody peekTableBody
+    ioProperty . roundtripEqual pushTableBody peekTableBodyFuzzy
 
   , testProperty "TableHead" $
     ioProperty . roundtripEqual pushTableHead peekTableHead
diff --git a/test/test-pandoc.lua b/test/test-pandoc.lua
--- a/test/test-pandoc.lua
+++ b/test/test-pandoc.lua
@@ -86,7 +86,6 @@
       }
       local tfoot = TableFoot()
       local tbl = Table(caption, colspecs, thead, {tbody}, tfoot)
-      print(tbl.bodies)
       local expected_body = tbody
       expected_body.body[2].cells:insert(Cell{})
       local doc = Pandoc{tbl}
