diff --git a/pandoc-lua-engine.cabal b/pandoc-lua-engine.cabal
--- a/pandoc-lua-engine.cabal
+++ b/pandoc-lua-engine.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 name:                pandoc-lua-engine
-version:             0.4.3
+version:             0.5
 build-type:          Simple
 license:             GPL-2.0-or-later
 license-file:        COPYING.md
@@ -93,6 +93,7 @@
                      , Text.Pandoc.Lua.Module.Log
                      , Text.Pandoc.Lua.Module.MediaBag
                      , Text.Pandoc.Lua.Module.Pandoc
+                     , Text.Pandoc.Lua.Module.Path
                      , Text.Pandoc.Lua.Module.Scaffolding
                      , Text.Pandoc.Lua.Module.Structure
                      , Text.Pandoc.Lua.Module.System
@@ -110,23 +111,23 @@
   build-depends:       aeson
                      , bytestring            >= 0.9     && < 0.13
                      , crypton               >= 0.30    && < 1.1
-                     , citeproc              >= 0.8     && < 0.10
+                     , citeproc              >= 0.8     && < 0.11
                      , containers            >= 0.6.0.1 && < 0.9
                      , data-default          >= 0.4     && < 0.9
                      , doclayout             >= 0.5     && < 0.6
                      , doctemplates          >= 0.11    && < 0.12
                      , exceptions            >= 0.8     && < 0.11
-                     , hslua                 >= 2.3     && < 2.4
+                     , hslua                 >= 2.3     && < 2.5
                      , hslua-module-doclayout>= 1.2     && < 1.3
                      , hslua-module-path     >= 1.1     && < 1.2
-                     , hslua-module-system   >= 1.1     && < 1.2
+                     , hslua-module-system   >= 1.2.3   && < 1.3
                      , hslua-module-text     >= 1.1     && < 1.2
                      , hslua-module-version  >= 1.1     && < 1.2
                      , hslua-module-zip      >= 1.1.3   && < 1.2
                      , hslua-repl            >= 0.1.1   && < 0.2
                      , lpeg                  >= 1.1     && < 1.2
                      , mtl                   >= 2.2     && < 2.4
-                     , pandoc                >= 3.7     && < 3.8
+                     , pandoc                >= 3.8     && < 3.9
                      , pandoc-lua-marshal    >= 0.3     && < 0.4
                      , pandoc-types          >= 1.22    && < 1.24
                      , parsec                >= 3.1     && < 3.2
@@ -144,7 +145,7 @@
                      , data-default
                      , exceptions            >= 0.8     && < 0.11
                      , filepath
-                     , hslua                 >= 2.3     && < 2.4
+                     , hslua                 >= 2.3     && < 2.5
                      , pandoc
                      , pandoc-types          >= 1.22    && < 1.24
                      , tasty
diff --git a/src/Text/Pandoc/Lua/Global.hs b/src/Text/Pandoc/Lua/Global.hs
--- a/src/Text/Pandoc/Lua/Global.hs
+++ b/src/Text/Pandoc/Lua/Global.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE LambdaCase         #-}
 {-# LANGUAGE OverloadedStrings  #-}
 {- |
    Module      : Text.Pandoc.Lua
@@ -16,14 +17,18 @@
 
 import HsLua as Lua
 import HsLua.Module.Version (pushVersion)
-import Text.Pandoc.Class (CommonState)
+import Text.Pandoc.Class ( getInputFiles, getOutputFile, getLog
+                         , getRequestHeaders, getResourcePath, getSourceURL
+                         , getUserDataDir, getTrace, getVerbosity
+                         )
 import Text.Pandoc.Definition (Pandoc, pandocTypesVersion)
 import Text.Pandoc.Error (PandocError)
-import Text.Pandoc.Lua.Marshal.CommonState (pushCommonState)
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Lua.Marshal.LogMessage (pushLogMessage)
 import Text.Pandoc.Lua.Marshal.Pandoc (pushPandoc)
 import Text.Pandoc.Lua.Marshal.ReaderOptions (pushReaderOptionsReadonly)
 import Text.Pandoc.Lua.Marshal.WriterOptions (pushWriterOptions)
-import Text.Pandoc.Lua.PandocLua ()
+import Text.Pandoc.Lua.PandocLua (unPandocLua)
 import Text.Pandoc.Options (ReaderOptions, WriterOptions)
 import Text.Pandoc.Version (pandocVersion)
 
@@ -37,7 +42,7 @@
   | PANDOC_READER_OPTIONS ReaderOptions
   | PANDOC_WRITER_OPTIONS WriterOptions
   | PANDOC_SCRIPT_FILE FilePath
-  | PANDOC_STATE CommonState
+  | PANDOC_STATE
   | PANDOC_VERSION
   -- Cannot derive instance of Data because of CommonState
 
@@ -66,8 +71,47 @@
   PANDOC_SCRIPT_FILE filePath -> do
     Lua.pushString filePath
     Lua.setglobal "PANDOC_SCRIPT_FILE"
-  PANDOC_STATE commonState -> do
-    pushCommonState commonState
+  PANDOC_STATE -> do
+    -- The common state is an opaque value. We provide a table that
+    -- contains the values accessible through the PandocMonad API. This
+    -- is for backwards compatibility, as the state used to be exposed
+    -- as a read-only object.
+    Lua.newtable
+    Lua.newmetatable "CommonStateInterface"
+    Lua.pushHaskellFunction $ do
+      Lua.forcePeek (peekText (Lua.nthBottom 2)) >>= \case
+        "input_files" -> do
+          pushPandocList pushString =<< unPandocLua getInputFiles
+          return 1
+        "output_file" -> do
+          maybe pushnil pushString =<< unPandocLua getOutputFile
+          return 1
+        "log" -> do
+          pushPandocList pushLogMessage =<< unPandocLua getLog
+          return 1
+        "request_headers" -> do
+          pushPandocList (pushPair pushText pushText)
+                   =<< unPandocLua getRequestHeaders
+          return 1
+        "resource_path" -> do
+          pushPandocList pushString =<< unPandocLua getResourcePath
+          return 1
+        "source_url" -> do
+          maybe pushnil pushText =<< unPandocLua getSourceURL
+          return 1
+        "user_data_dir" -> do
+          maybe pushnil pushString =<< unPandocLua getUserDataDir
+          return 1
+        "trace" -> do
+          pushBool =<< unPandocLua getTrace
+          return 1
+        "verbosity" -> do
+          pushString . show =<< unPandocLua getVerbosity
+          return 1
+        _ ->
+          failLua "Unknown key"
+    Lua.setfield (Lua.nth 2) "__index"
+    Lua.setmetatable (Lua.nth 2)
     Lua.setglobal "PANDOC_STATE"
   PANDOC_VERSION              -> do
     pushVersion pandocVersion
diff --git a/src/Text/Pandoc/Lua/Marshal/CommonState.hs b/src/Text/Pandoc/Lua/Marshal/CommonState.hs
--- a/src/Text/Pandoc/Lua/Marshal/CommonState.hs
+++ b/src/Text/Pandoc/Lua/Marshal/CommonState.hs
@@ -16,41 +16,15 @@
   ) where
 
 import HsLua
-import Text.Pandoc.Class (CommonState (..))
-import Text.Pandoc.Lua.Marshal.List (pushPandocList)
-import Text.Pandoc.Lua.Marshal.LogMessage (pushLogMessage)
+import Text.Pandoc.Class (CommonState)
 
 -- | Lua type used for the @CommonState@ object.
+--
+-- This is an opaque value that is required for the Lua interpreter
+-- to become an instance of "PandocMonad".
+--
 typeCommonState :: LuaError e => DocumentedType e CommonState
-typeCommonState = deftype "CommonState" []
-  [ readonly "input_files" "input files passed to pandoc"
-      (pushPandocList pushString, stInputFiles)
-
-  , readonly "output_file" "the file to which pandoc will write"
-      (maybe pushnil pushString, stOutputFile)
-
-  , readonly "log" "list of log messages"
-      (pushPandocList pushLogMessage, stLog)
-
-  , readonly "request_headers" "headers to add for HTTP requests"
-      (pushPandocList (pushPair pushText pushText), stRequestHeaders)
-
-  , readonly "resource_path"
-      "path to search for resources like included images"
-      (pushPandocList pushString, stResourcePath)
-
-  , readonly "source_url" "absolute URL + dir of 1st source file"
-      (maybe pushnil pushText, stSourceURL)
-
-  , readonly "user_data_dir" "directory to search for data files"
-      (maybe pushnil pushString, stUserDataDir)
-
-  , readonly "trace" "controls whether tracing messages are issued"
-      (pushBool, stTrace)
-
-  , readonly "verbosity" "verbosity level"
-      (pushString . show, stVerbosity)
-  ]
+typeCommonState = deftype "CommonState" [] []
 
 peekCommonState :: LuaError e => Peeker e CommonState
 peekCommonState = peekUD typeCommonState
diff --git a/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs b/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs
--- a/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs
+++ b/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs
@@ -115,10 +115,10 @@
     (pushExtensions, writerExtensions)
     (peekExtensions, \opts x -> opts{ writerExtensions = x })
 
-  , property "highlight_style"
-    "Style to use for highlighting (nil = no highlighting)"
-    (maybe pushnil pushViaJSON, writerHighlightStyle)
-    (optional . peekViaJSON, \opts x -> opts{ writerHighlightStyle = x })
+  , property "highlight_method"
+    "Method to use for code highlighting ('none'|'default'|'idiomatic'|style)"
+    (pushViaJSON, writerHighlightMethod)
+    (peekViaJSON, \opts x -> opts{ writerHighlightMethod = x })
 
   , property "html_math_method"
     "How to print math in HTML"
@@ -154,11 +154,6 @@
     "Include list of tables"
     (pushBool, writerListOfTables)
     (peekBool, \opts x -> opts{ writerListOfTables = x })
-
-  , property "listings"
-    "Use listings package for code"
-    (pushBool, writerListings)
-    (peekBool, \opts x -> opts{ writerListings = x })
 
   , property "number_offset"
     "Starting number for section, subsection, ..."
diff --git a/src/Text/Pandoc/Lua/Module.hs b/src/Text/Pandoc/Lua/Module.hs
--- a/src/Text/Pandoc/Lua/Module.hs
+++ b/src/Text/Pandoc/Lua/Module.hs
@@ -23,7 +23,6 @@
 import qualified Lua.LPeg as LPeg
 import qualified HsLua.Aeson
 import qualified HsLua.Module.DocLayout as Module.Layout
-import qualified HsLua.Module.Path as Module.Path
 import qualified HsLua.Module.Zip as Module.Zip
 import qualified Text.Pandoc.Lua.Module.CLI as Pandoc.CLI
 import qualified Text.Pandoc.Lua.Module.Format as Pandoc.Format
@@ -32,6 +31,7 @@
 import qualified Text.Pandoc.Lua.Module.Log as Pandoc.Log
 import qualified Text.Pandoc.Lua.Module.MediaBag as Pandoc.MediaBag
 import qualified Text.Pandoc.Lua.Module.Pandoc as Module.Pandoc
+import qualified Text.Pandoc.Lua.Module.Path as Pandoc.Path
 import qualified Text.Pandoc.Lua.Module.Scaffolding as Pandoc.Scaffolding
 import qualified Text.Pandoc.Lua.Module.Structure as Pandoc.Structure
 import qualified Text.Pandoc.Lua.Module.System as Pandoc.System
@@ -84,6 +84,7 @@
   , Pandoc.JSON.documentedModule
   , Pandoc.Log.documentedModule
   , Pandoc.MediaBag.documentedModule
+  , Pandoc.Path.documentedModule
   , Pandoc.Scaffolding.documentedModule
   , Pandoc.Structure.documentedModule
   , Pandoc.System.documentedModule
@@ -95,8 +96,6 @@
       `allSince` [2,18])
      `functionsSince` ["bold", "italic", "underlined", "strikeout", "fg", "bg"])
     [3, 4, 1]
-  , Module.Path.documentedModule { moduleName = "pandoc.path" }
-    `allSince` [2,12]
   , Module.Zip.documentedModule { moduleName = "pandoc.zip" }
     `allSince` [3,0]
   ]
diff --git a/src/Text/Pandoc/Lua/Module/Log.hs b/src/Text/Pandoc/Lua/Module/Log.hs
--- a/src/Text/Pandoc/Lua/Module/Log.hs
+++ b/src/Text/Pandoc/Lua/Module/Log.hs
@@ -14,14 +14,9 @@
 
 import Data.Version (makeVersion)
 import HsLua
-import Text.Pandoc.Class
-  ( CommonState (stVerbosity, stLog)
-  , PandocMonad (putCommonState, getCommonState)
-  , report )
+import Text.Pandoc.Class (report, runSilently)
 import Text.Pandoc.Error (PandocError)
-import Text.Pandoc.Logging
-  ( Verbosity (ERROR)
-  , LogMessage (ScriptingInfo, ScriptingWarning) )
+import Text.Pandoc.Logging (LogMessage (ScriptingInfo, ScriptingWarning))
 import Text.Pandoc.Lua.Marshal.List (pushPandocList)
 import Text.Pandoc.Lua.Marshal.LogMessage (pushLogMessage)
 import Text.Pandoc.Lua.PandocLua (liftPandocLua, unPandocLua)
@@ -92,23 +87,12 @@
 -- results of the function call after that.
 silence :: LuaE PandocError NumResults
 silence = unPandocLua $ do
-  -- get current log messages
-  origState <- getCommonState
-  let origLog = stLog origState
-  let origVerbosity = stVerbosity origState
-  putCommonState (origState { stLog = [], stVerbosity = ERROR })
-
   -- call function given as the first argument
-  liftPandocLua $ do
+  ((), messages) <- runSilently . liftPandocLua $ do
     nargs <- (NumArgs . subtract 1 . fromStackIndex) <$> gettop
     call @PandocError nargs multret
 
-  -- restore original log messages
-  newState <- getCommonState
-  let newLog = stLog newState
-  putCommonState (newState { stLog = origLog, stVerbosity = origVerbosity })
-
   liftPandocLua $ do
-    pushPandocList pushLogMessage newLog
+    pushPandocList pushLogMessage messages
     insert 1
     (NumResults . fromStackIndex) <$> gettop
diff --git a/src/Text/Pandoc/Lua/Module/MediaBag.hs b/src/Text/Pandoc/Lua/Module/MediaBag.hs
--- a/src/Text/Pandoc/Lua/Module/MediaBag.hs
+++ b/src/Text/Pandoc/Lua/Module/MediaBag.hs
@@ -18,8 +18,7 @@
 import HsLua ( LuaE, DocumentedFunction, Module (..)
              , (<#>), (###), (=#>), (=?>), (#?), defun, functionResult
              , opt, parameter, since, stringParam, textParam)
-import Text.Pandoc.Class ( CommonState (..), fetchItem, fillMediaBag
-                         , getMediaBag, modifyCommonState, setMediaBag)
+import Text.Pandoc.Class ( fetchItem, fillMediaBag, getMediaBag, setMediaBag )
 import Text.Pandoc.Class.IO (writeMedia)
 import Text.Pandoc.Error (PandocError)
 import Text.Pandoc.Lua.Marshal.Pandoc (peekPandoc, pushPandoc)
@@ -71,8 +70,9 @@
 -- | Delete a single item from the media bag.
 delete :: DocumentedFunction PandocError
 delete = defun "delete"
-  ### (\fp -> unPandocLua $ modifyCommonState
-              (\st -> st { stMediaBag = MB.deleteMedia fp (stMediaBag st) }))
+  ### (\fp -> unPandocLua $ do
+          mb <- getMediaBag
+          setMediaBag $ MB.deleteMedia fp mb)
   <#> stringParam "filepath"
       ("Filename of the item to deleted. The media bag will be " <>
        "left unchanged if no entry with the given filename exists.")
@@ -82,7 +82,7 @@
 -- | Delete all items from the media bag.
 empty :: DocumentedFunction PandocError
 empty = defun "empty"
-  ### unPandocLua (modifyCommonState (\st -> st { stMediaBag = mempty }))
+  ### unPandocLua (setMediaBag mempty)
   =#> []
   #? "Clear-out the media bag, deleting all items."
 
diff --git a/src/Text/Pandoc/Lua/Module/Path.hs b/src/Text/Pandoc/Lua/Module/Path.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Pandoc/Lua/Module/Path.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications    #-}
+{- |
+   Module      : Text.Pandoc.Lua.Module.Path
+   Copyright   : © 2019-2024 Albert Krewinkel
+   License     : GNU GPL, version 2 or above
+
+   Maintainer  : Albert Krewinkel <albert+pandoc@tarleb.com>
+   Stability   : alpha
+
+Pandoc's system Lua module.
+-}
+module Text.Pandoc.Lua.Module.Path
+  ( documentedModule
+  ) where
+
+import Data.Version (makeVersion)
+import HsLua
+import qualified HsLua.Module.Path   as MPath
+import qualified HsLua.Module.System as MSystem
+
+-- | Push the pandoc.system module on the Lua stack.
+documentedModule :: forall e. LuaError e => Module e
+documentedModule = Module
+  { moduleName = "pandoc.path"
+  , moduleDescription = moduleDescription @e MPath.documentedModule
+  , moduleFields =
+      [ MPath.separator
+      , MPath.search_path_separator
+      ]
+  , moduleFunctions =
+      [ MPath.directory              `since` v[2,12]
+      , MSystem.exists               `since` v[3,7,1]
+      , MPath.filename               `since` v[2,12]
+      , MPath.is_absolute            `since` v[2,12]
+      , MPath.is_relative            `since` v[2,12]
+      , MPath.join                   `since` v[2,12]
+      , MPath.make_relative          `since` v[2,12]
+      , MPath.normalize              `since` v[2,12]
+      , MPath.split                  `since` v[2,12]
+      , MPath.split_extension        `since` v[2,12]
+      , MPath.split_search_path      `since` v[2,12]
+      , MPath.treat_strings_as_paths `since` v[2,12]
+      ]
+  , moduleOperations = []
+  , moduleTypeInitializers = []
+  }
+ where
+  v = makeVersion
diff --git a/src/Text/Pandoc/Lua/Module/Structure.hs b/src/Text/Pandoc/Lua/Module/Structure.hs
--- a/src/Text/Pandoc/Lua/Module/Structure.hs
+++ b/src/Text/Pandoc/Lua/Module/Structure.hs
@@ -2,7 +2,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {- |
    Module      : Text.Pandoc.Lua.Module.Structure
-   Copyright   : © 2023 Albert Krewinkel
+   Copyright   : © 2023-2025 Albert Krewinkel <albert+pandoc@tarleb.com>
    License     : GPL-2.0-or-later
    Maintainer  : Albert Krewinkel <albert+pandoc@tarleb.com>
 
@@ -20,15 +20,17 @@
              , (###), (<#>), (=#>), (#?)
              , defun, functionResult, getfield, isnil, lastly, liftLua
              , opt, liftPure, parameter , peekBool, peekIntegral
-             , peekFieldRaw, peekText, pop, pushIntegral, since, top )
+             , peekFieldRaw, peekSet, peekText, pop, pushIntegral
+             , pushText, since, top )
 import Text.Pandoc.Chunks ( ChunkedDoc (..), PathTemplate (..)
                           , tocToList, splitIntoChunks )
 import Text.Pandoc.Definition (Pandoc (..), Block)
 import Text.Pandoc.Error (PandocError)
 import Text.Pandoc.Lua.PandocLua ()
-import Text.Pandoc.Lua.Marshal.AST ( peekBlocksFuzzy, peekPandoc
-                                   , pushBlock, pushBlocks )
+import Text.Pandoc.Lua.Marshal.AST ( peekBlocksFuzzy, peekInlinesFuzzy
+                                   , peekPandoc, pushBlock, pushBlocks )
 import Text.Pandoc.Lua.Marshal.Chunks
+import Text.Pandoc.Lua.Marshal.Format (peekExtensions)
 import Text.Pandoc.Lua.Marshal.WriterOptions ( peekWriterOptions )
 import Text.Pandoc.Options (WriterOptions (writerTOCDepth,
                                            writerNumberSections))
@@ -50,6 +52,7 @@
       , slide_level       `since` makeVersion [3,0]
       , split_into_chunks `since` makeVersion [3,0]
       , table_of_contents `since` makeVersion [3,0]
+      , unique_identifier `since` makeVersion [3,8]
       ]
   , moduleOperations = []
   , moduleTypeInitializers = []
@@ -197,6 +200,40 @@
   peekTocSource idx =
     (Left <$> peekBodyBlocks idx) <|>
     (Right . chunkedTOC <$> peekChunkedDoc idx)
+
+-- | Generate a unique ID from a list of inlines.
+unique_identifier :: LuaError e => DocumentedFunction e
+unique_identifier = defun "unique_identifier"
+  ### (\inlns mUsedIdents mExts -> do
+          let usedIdents = fromMaybe mempty mUsedIdents
+          let exts       = fromMaybe mempty mExts
+          pure $ Shared.uniqueIdent exts inlns usedIdents)
+  <#> parameter peekInlinesFuzzy "Inlines" "inlines" "base for identifier"
+  <#> opt (parameter (peekSet peekText) "table" "used"
+           "set of identifiers (string keys, boolean values) that\
+           \ have already been used.")
+  <#> opt (parameter peekExtensions "{string,...}" "exts"
+           "list of format extensions")
+  =#> functionResult pushText "string" "unique identifier"
+  #? "Generates a unique identifier from a list of inlines, similar to\
+     \ what's generated by the `auto_identifiers` extension.\n\
+     \\n\
+     \ The method used to generated identifiers can be modified through\
+     \ `ext`, which is a list of format extensions.\n\
+     \\n\
+     \ It can be used to generate IDs similar to what the `auto_identifiers`\
+     \ extension provides.\n\
+     \\n\
+     \ Example:\n\
+     \\n\
+     \     local used_ids = {}\n\
+     \     function Header (h)\n\
+     \       local id =\n\
+     \         pandoc.structure.unique_identifier(h.content, used_ids)\n\
+     \       used_ids[id] = true\n\
+     \       h.identifier = id\n\
+     \       return h\n\
+     \     end"
 
 -- | Retrieves the body blocks of a 'Pandoc' object or from a list of
 -- blocks.
diff --git a/src/Text/Pandoc/Lua/Module/System.hs b/src/Text/Pandoc/Lua/Module/System.hs
--- a/src/Text/Pandoc/Lua/Module/System.hs
+++ b/src/Text/Pandoc/Lua/Module/System.hs
@@ -18,8 +18,10 @@
 import Data.Version (makeVersion)
 import HsLua
 import HsLua.Module.System
-  ( arch, cputime, env, getwd, ls, mkdir, os, rmdir
-  , with_env, with_tmpdir, with_wd)
+  ( arch, cmd, cp, cputime, env, getwd, ls, mkdir, os, read_file
+  , rename, rm, rmdir, times, with_env, with_tmpdir, with_wd
+  , write_file, xdg
+  )
 import qualified HsLua.Module.System as MSys
 
 -- | Push the pandoc.system module on the Lua stack.
@@ -33,14 +35,22 @@
       ]
   , moduleFunctions =
       [ cputime                                        `since` v[3,1,1]
+      , setName "command" cmd                          `since` v[3,7,1]
+      , setName "copy" cp                              `since` v[3,7,1]
       , setName "environment" env                      `since` v[2,7,3]
       , setName "get_working_directory" getwd          `since` v[2,8]
       , setName "list_directory" ls                    `since` v[2,19]
       , setName "make_directory" mkdir                 `since` v[2,19]
+      , read_file                                      `since` v[3,7,1]
+      , rename                                         `since` v[3,7,1]
+      , setName "remove" rm                            `since` v[3,7,1]
       , setName "remove_directory" rmdir               `since` v[2,19]
+      , times                                          `since` v[3,7,1]
       , setName "with_environment" with_env            `since` v[2,7,3]
       , setName "with_temporary_directory" with_tmpdir `since` v[2,8]
       , setName "with_working_directory" with_wd       `since` v[2,7,3]
+      , write_file                                     `since` v[3,7,1]
+      , xdg                                            `since` v[3,7,1]
       ]
   , moduleOperations = []
   , moduleTypeInitializers = []
diff --git a/src/Text/Pandoc/Lua/Module/Text.hs b/src/Text/Pandoc/Lua/Module/Text.hs
--- a/src/Text/Pandoc/Lua/Module/Text.hs
+++ b/src/Text/Pandoc/Lua/Module/Text.hs
@@ -15,6 +15,7 @@
 import HsLua
 import Text.Pandoc.Error (PandocError)
 import Text.Pandoc.Lua.PandocLua ()
+import Text.Pandoc.Writers.Shared (toSubscript, toSuperscript)
 
 import qualified Data.Text as T
 import qualified HsLua.Module.Text as TM
@@ -29,6 +30,8 @@
     , TM.lower        `since` v[2,0,3]
     , TM.reverse      `since` v[2,0,3]
     , TM.sub          `since` v[2,0,3]
+    , subscript       `since` v[3,8]
+    , superscript     `since` v[3,8]
     , TM.toencoding   `since` v[3,0]
     , TM.upper        `since` v[2,0,3]
     ]
@@ -49,3 +52,29 @@
   }
  where
   v = makeVersion
+
+-- | Convert all chars in a string to Unicode subscript.
+subscript :: LuaError e => DocumentedFunction e
+subscript = defun "subscript"
+  ### pure . traverse toSubscript
+  <#> stringParam "input" "string to convert to subscript characters"
+  =#> functionResult (maybe pushnil pushString) "string|nil"
+      "Subscript version of the input, or `nil` if not all characters\
+      \ could be converted."
+  #? "Tries to convert the string into a Unicode subscript version of the\
+     \ string.  Returns `nil` if not all characters of the input can be\
+     \ mapped to a subscript Unicode character.\
+     \ Supported characters include numbers, parentheses, and plus/minus."
+
+-- | Convert all chars in a string to Unicode superscript.
+superscript :: LuaError e => DocumentedFunction e
+superscript = defun "superscript"
+  ### pure . traverse toSuperscript
+  <#> stringParam "input" "string to convert to superscript characters"
+  =#> functionResult (maybe pushnil pushString) "string|nil"
+      "Superscript version of the input, or `nil` if not all characters\
+      \ could be converted."
+  #? "Tries to convert the string into a Unicode superscript version of the\
+     \ string.  Returns `nil` if not all characters of the input can be\
+     \ mapped to a superscript Unicode character.\
+     \ Supported characters include numbers, parentheses, and plus/minus."
diff --git a/src/Text/Pandoc/Lua/PandocLua.hs b/src/Text/Pandoc/Lua/PandocLua.hs
--- a/src/Text/Pandoc/Lua/PandocLua.hs
+++ b/src/Text/Pandoc/Lua/PandocLua.hs
@@ -81,9 +81,7 @@
     forcePeek $ peekCommonState Lua.top `lastly` pop 1
   putCommonState cst = PandocLua $ do
     pushCommonState cst
-    Lua.pushvalue Lua.top
     Lua.setfield registryindex "PANDOC_STATE"
-    Lua.setglobal "PANDOC_STATE"
 
   logOutput = IO.logOutput
 
diff --git a/src/Text/Pandoc/Lua/Run.hs b/src/Text/Pandoc/Lua/Run.hs
--- a/src/Text/Pandoc/Lua/Run.hs
+++ b/src/Text/Pandoc/Lua/Run.hs
@@ -61,7 +61,7 @@
                  -> m a
 runPandocLuaWith runner pLua = do
   origState <- getCommonState
-  globals <- defaultGlobals
+  let globals = defaultGlobals
   (result, newState) <- liftIO . runner . unPandocLua $ do
     putCommonState origState
     liftPandocLua $ setGlobals globals
@@ -72,11 +72,9 @@
   return result
 
 -- | Global variables which should always be set.
-defaultGlobals :: PandocMonad m => m [Global]
-defaultGlobals = do
-  commonState <- getCommonState
-  return
-    [ PANDOC_API_VERSION
-    , PANDOC_STATE commonState
-    , PANDOC_VERSION
-    ]
+defaultGlobals :: [Global]
+defaultGlobals =
+  [ PANDOC_API_VERSION
+  , PANDOC_STATE
+  , PANDOC_VERSION
+  ]
diff --git a/test/Tests/Lua.hs b/test/Tests/Lua.hs
--- a/test/Tests/Lua.hs
+++ b/test/Tests/Lua.hs
@@ -24,8 +24,7 @@
                             linebreak, math, orderedList, para, plain, rawBlock,
                             singleQuoted, space, str, strong,
                             HasMeta (setMeta))
-import Text.Pandoc.Class ( CommonState (stVerbosity)
-                         , modifyCommonState, runIOorExplode, setUserDataDir)
+import Text.Pandoc.Class ( runIOorExplode, setUserDataDir, setVerbosity )
 import Text.Pandoc.Definition (Attr, Block (BlockQuote, Div, Para), Pandoc,
                                Inline (Emph, Str), pandocTypesVersion)
 import Text.Pandoc.Error (PandocError (PandocLuaError))
@@ -242,7 +241,7 @@
 runLuaTest op = runIOorExplode $ do
   -- Disable printing of warnings on stderr: some tests will generate
   -- warnings, we don't want to see those messages.
-  modifyCommonState $ \st -> st { stVerbosity = ERROR }
+  setVerbosity ERROR
   res <- runLua $ do
     setGlobals [ PANDOC_WRITER_OPTIONS def ]
     op
diff --git a/test/lua/module/globals.lua b/test/lua/module/globals.lua
--- a/test/lua/module/globals.lua
+++ b/test/lua/module/globals.lua
@@ -41,8 +41,8 @@
         assert.are_equal(type(v), 'string')
       end
     end),
-    test('highlight_style', function ()
-      assert.are_equal(type(PANDOC_WRITER_OPTIONS.highlight_style), 'table')
+    test('highlight_method', function ()
+      assert.are_equal(type(PANDOC_WRITER_OPTIONS.highlight_method), 'string')
     end),
     test('html_math_method', function ()
       assert.are_equal(type(PANDOC_WRITER_OPTIONS.html_math_method), 'string')
@@ -56,9 +56,6 @@
     test('incremental', function ()
       assert.are_equal(type(PANDOC_WRITER_OPTIONS.incremental), 'boolean')
     end),
-    test('listings', function ()
-      assert.are_equal(type(PANDOC_WRITER_OPTIONS.listings), 'boolean')
-    end),
     test('number_offset', function ()
       assert.are_equal(type(PANDOC_WRITER_OPTIONS.number_offset), 'table')
       for _, v in ipairs(PANDOC_WRITER_OPTIONS.number_offset) do
@@ -110,8 +107,8 @@
   },
 
   group 'PANDOC_STATE' {
-    test('is a userdata object', function ()
-      assert.are_equal(type(PANDOC_STATE), 'userdata')
+    test('is a table object', function ()
+      assert.are_equal(type(PANDOC_STATE), 'table')
     end),
     test('has property "input_files"', function ()
       assert.are_equal(type(PANDOC_STATE.input_files), 'table')
diff --git a/test/lua/module/pandoc-format.lua b/test/lua/module/pandoc-format.lua
--- a/test/lua/module/pandoc-format.lua
+++ b/test/lua/module/pandoc-format.lua
@@ -42,6 +42,8 @@
         fancy_lists = false,
         gfm_auto_identifiers = false,
         smart = false,
+        smart_quotes = false,
+        special_strings = true,
         task_lists = true,
       }
       assert.are_same(format.extensions 'org', org_default_exts)
diff --git a/test/lua/module/pandoc-log.lua b/test/lua/module/pandoc-log.lua
--- a/test/lua/module/pandoc-log.lua
+++ b/test/lua/module/pandoc-log.lua
@@ -22,13 +22,13 @@
     end),
     test('reports a warning', function ()
       log.info('info test')
-      local msg = json.decode(json.encode(PANDOC_STATE.log[1]))
+      local msg = json.decode(json.encode(PANDOC_STATE.log:at(-1)))
       assert.are_equal(msg.message, 'info test')
       assert.are_equal(msg.type, 'ScriptingInfo')
     end),
     test('info includes the correct number', function ()
       log.info('line number test')
-      local msg = json.decode(json.encode(PANDOC_STATE.log[1]))
+      local msg = json.decode(json.encode(PANDOC_STATE.log:at(-1)))
       -- THIS NEEDS UPDATING if lines above are shifted.
       assert.are_equal(msg.line, 30)
     end),
@@ -40,7 +40,7 @@
     end),
     test('reports a warning', function ()
       log.warn('testing')
-      local msg = json.decode(json.encode(PANDOC_STATE.log[1]))
+      local msg = json.decode(json.encode(PANDOC_STATE.log:at(-1)))
       assert.are_equal(msg.message, 'testing')
       assert.are_equal(msg.type, 'ScriptingWarning')
     end),
diff --git a/test/lua/module/pandoc-structure.lua b/test/lua/module/pandoc-structure.lua
--- a/test/lua/module/pandoc-structure.lua
+++ b/test/lua/module/pandoc-structure.lua
@@ -136,4 +136,28 @@
       )
     end),
   },
+  group 'unique_identifier' {
+    test('returns an identifier based on the input', function ()
+      local inlines = pandoc.Inlines{pandoc.Emph{'This'}, ' is nice'}
+      local id = structure.unique_identifier(inlines)
+      assert.are_equal('this-is-nice', id)
+    end),
+    test('respects the list of used IDs', function ()
+      local inlines = pandoc.Inlines('Hello, World!')
+      local used = {['hello-world'] = true}
+      local id = structure.unique_identifier(inlines, used)
+      assert.are_equal('hello-world-1', id)
+    end),
+    test('defaults to pandoc Markdown identifiers', function ()
+      local inlines = pandoc.Inlines('Mr. Jones')
+      local id = structure.unique_identifier(inlines, {})
+      assert.are_equal('mr.-jones', id)
+    end),
+    test('can generate gfm identifiers', function ()
+      local inlines = pandoc.Inlines('Mr. Jones')
+      local exts = {'gfm_auto_identifiers'}
+      local id = structure.unique_identifier(inlines, {}, exts)
+      assert.are_equal('mr-jones', id)
+    end),
+  }
 }
diff --git a/test/lua/module/pandoc-text.lua b/test/lua/module/pandoc-text.lua
--- a/test/lua/module/pandoc-text.lua
+++ b/test/lua/module/pandoc-text.lua
@@ -41,6 +41,28 @@
     test('sub', function ()
       assert.is_function(text.sub)
     end),
+    group 'subscript' {
+      test('is a function', function ()
+        assert.is_function(text.subscript)
+      end),
+      test('converts a string to Unicode subscript chars', function ()
+        assert.are_equal(text.subscript '1+(9-7)', '₁₊₍₉₋₇₎')
+      end),
+      test('returns nil if the input contains unsupported chars', function ()
+        assert.is_nil(text.subscript '00ä')
+      end),
+    },
+    group 'superscript' {
+      test('is a function', function ()
+        assert.is_function(text.superscript)
+      end),
+      test('converts a string to Unicode superscript chars', function ()
+        assert.are_equal(text.superscript '1+(9-7)', '¹⁺⁽⁹⁻⁷⁾')
+      end),
+      test('returns nil if the input contains unsupported chars', function ()
+        assert.is_nil(text.superscript '00ä')
+      end),
+    },
     test('toencoding', function ()
       assert.is_function(text.toencoding)
     end),
diff --git a/test/lua/module/pandoc.lua b/test/lua/module/pandoc.lua
--- a/test/lua/module/pandoc.lua
+++ b/test/lua/module/pandoc.lua
@@ -164,6 +164,13 @@
       assert.are_same(meta.test, {pandoc.Plain{pandoc.Str 'check'}})
     end),
   },
+  group 'Pandoc' {
+    test('normalize', function ()
+      local doc = pandoc.Pandoc({{'a', pandoc.Space(), pandoc.Space(), 'b'}})
+      local normalized = pandoc.Pandoc({{'a', pandoc.Space(), 'b'}})
+      assert.are_equal(normalized, doc:normalize())
+    end),
+  },
   group 'Other types' {
     group 'ReaderOptions' {
       test('returns a userdata value', function ()
