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.2.9
+
+Released 2024-10-01.
+
+-   Update list module, thereby introducing a new method `iter`;
+    the function returns an iterator that steps through list
+    values on each call.
+
+-   Added support for `__toinline` and `__toblock` metamethods. If
+    the metamethods are available on a Lua object and return an
+    appropriate value, then that returned value will be used
+    during unmarshalling.
+
 ## 0.2.8
 
 Released 2024-09-21.
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.2.8
+version:             0.2.9
 synopsis:            Use pandoc types in Lua
 description:         This package provides functions to marshal and unmarshal
                      pandoc document types to and from Lua.
@@ -51,7 +51,7 @@
                      , containers            >= 0.6      && < 0.8
                      , exceptions            >= 0.8      && < 0.11
                      , hslua                 >= 2.2      && < 2.4
-                     , hslua-list            >= 1.1.2    && < 1.2
+                     , hslua-list            >= 1.1.4    && < 1.2
                      , hslua-marshalling     >= 2.2      && < 2.4
                      , pandoc-types          >= 1.23     && < 1.24
                      , safe                  >= 0.3      && < 0.4
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
@@ -109,12 +109,32 @@
   setmetatable (nth 2)
 {-# INLINABLE pushBlocks #-}
 
+-- | Unmarshal a table as Block value by calling the @__toblock@ metamethod
+-- first.
+peekBlockMetamethod :: LuaError e
+                    => Peeker e Block
+peekBlockMetamethod idx = do
+  absidx <- liftLua $ absindex idx
+  liftLua (getmetafield absidx "__toblock") >>= \case
+    TypeNil      -> failPeek "object has no __toblock metamethod"
+    TypeFunction -> do
+      liftLua (pushvalue absidx)
+      liftLua (pcall 1 1 Nothing) >>= \case
+        OK   -> peekBlock top `lastly` pop 1
+        _err -> do
+          msg <- peekByteString top `lastly` pop 1
+          failPeek $ "failure in __toblock: " <> msg
+    _otherType   -> do
+      liftLua (pop 1)   -- drop "__toblock" field
+      failPeek "__toblock metafield does not contain a function"
+
 -- | Try extra hard to retrieve an Block value from the stack. Treats
 -- bare strings as @Str@ values.
 peekBlockFuzzy :: LuaError e
                => Peeker e Block
 peekBlockFuzzy idx =
        peekBlock idx
+  <|> peekBlockMetamethod idx
   <|> (Plain <$!> peekInlinesFuzzy idx)
   <|> (failPeek =<<
        typeMismatchMessage "Block or list of Inlines" idx)
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
@@ -103,12 +103,32 @@
   setmetatable (nth 2)
 {-# INLINABLE pushInlines #-}
 
+-- | Unmarshal a value as Inline object by first calling the @__toinline@
+-- metamethod on that object.
+peekInlineMetamethod :: LuaError e
+                    => Peeker e Inline
+peekInlineMetamethod idx = do
+  absidx <- liftLua $ absindex idx
+  liftLua (getmetafield absidx "__toinline") >>= \case
+    TypeNil      -> failPeek "object has no __toinline metamethod"
+    TypeFunction -> do
+      liftLua (pushvalue absidx)
+      liftLua (pcall 1 1 Nothing) >>= \case
+        OK   -> peekInline top `lastly` pop 1
+        _err -> do
+          msg <- peekByteString top `lastly` pop 1
+          failPeek $ "failure in __toinline: " <> msg
+    _otherType   -> do
+      liftLua (pop 1)   -- drop "__toinline" field
+      failPeek "__toinline metafield does not contain a function"
+
 -- | Try extra hard to retrieve an Inline value from the stack. Treats
 -- bare strings as @Str@ values.
 peekInlineFuzzy :: LuaError e => Peeker e Inline
 peekInlineFuzzy idx = retrieving "Inline" $ liftLua (ltype idx) >>= \case
   TypeString   -> Str <$!> peekText idx
-  _            -> peekInline idx
+  TypeTable    -> peekInlineMetamethod idx <|> peekInline idx
+  _            -> peekInline idx <|> peekInlineMetamethod idx
 {-# INLINABLE peekInlineFuzzy #-}
 
 -- | Try extra-hard to return the value at the given index as a list of
diff --git a/test/test-block.lua b/test/test-block.lua
--- a/test/test-block.lua
+++ b/test/test-block.lua
@@ -670,5 +670,46 @@
         names
       )
     end),
+  },
+
+  group 'Block marshalling' {
+    test('Inlines are unmarshalled as Plain', function ()
+      assert.are_equal(Blocks{Plain{'a'}}, Blocks{Inlines{Str 'a'}})
+    end),
+
+    group '__toblock metamethod' {
+      test('metamethod __toblock is called when available', function ()
+        local function toblock (t)
+          return CodeBlock(t.code, {id = t.id, class = t.class})
+        end
+        local my_code = setmetatable(
+          {code = 'open access', id='opn'},
+          {__toblock = toblock}
+        )
+        assert.are_equal(
+          Div{CodeBlock('open access', {'opn'})},
+          Div{my_code}
+        )
+      end),
+
+      test("metafield is ignored if it's not a function", function ()
+        local bad_block = setmetatable({'a'}, {__toblock = true})
+        assert.are_equal(
+          Blocks{Plain{'a'}, Plain{'b'}},
+          Blocks{bad_block, {'b'}}
+        )
+      end),
+
+      test("non-Block return values are ignored", function ()
+        local function toblock ()
+          return "not a block"
+        end
+        local bad_block = setmetatable({'a'}, {__toblock = toblock})
+        assert.are_equal(
+          Blocks{Plain{'b'}, Plain{'a'}},
+          Blocks{Plain{'b'}, bad_block}
+        )
+      end),
+    }
   }
 }
diff --git a/test/test-inline.lua b/test/test-inline.lua
--- a/test/test-inline.lua
+++ b/test/test-inline.lua
@@ -457,5 +457,46 @@
         table.concat(names, ', ')
       )
     end),
+  },
+
+  group 'marshalling' {
+    test('bare strings become Str values', function ()
+      assert.are_equal(Inlines{'a'}, Inlines{Str 'a'})
+    end),
+
+    group '__toinline metamethod' {
+      test('metamethod __toinline is called when available', function ()
+        local function toinline (t)
+          return Code(t.code, {id = t.id, class = t.class})
+        end
+        local my_code = setmetatable(
+          {code = 'open access', id='opn'},
+          {__toinline = toinline}
+        )
+        assert.are_equal(
+          Inlines{Code('open access', {'opn'})},
+          Inlines{my_code}
+        )
+      end),
+
+      test("metafield is ignored if it's not a function", function ()
+        local bad_inline = setmetatable({'a'}, {__toinline = true})
+        assert.are_equal(
+          Inlines({'a'}),
+          Inlines(bad_inline)
+        )
+      end),
+
+      test("non-Inline return values are ignored", function ()
+        local function toinline ()
+          return "not an inline"
+        end
+        local bad_inline = setmetatable({'a'}, {__toinline = toinline})
+        assert.are_equal(
+          Inlines({'a'}),
+          Inlines(bad_inline)
+        )
+      end),
+    }
   }
 }
