diff --git a/.gitignore b/.gitignore
index 6dd5f1775..4596713be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ TAGS
 /tools/pandoc-drop-links
 /tools/pandoc-drop-notes
 /tools/pandoc-drop-toc
+/tools/generatetimeclock
 
 # Tricksy rules ignoring some typical temp files.
 # For troubleshooting: git check-ignore --verbose PATHS...
@@ -73,4 +74,3 @@ hledger-web/yesod-devel/
 .hledger-web_client_session_key.aes
 
 # recent stuff
-
diff --git a/Shake.hs b/Shake.hs
index 672b1fa4c..72c21a27d 100755
--- a/Shake.hs
+++ b/Shake.hs
@@ -67,7 +67,6 @@ usage = unlines
   ]
 
 pandoc = "stack exec -- pandoc" -- pandoc from project's stackage snapshot
-pandocSiteFilter = "tools/pandoc-site"
 makeinfo = "makeinfo"
 -- nroff = "nroff"
 groff = "groff"
@@ -297,11 +296,6 @@ main = do
     phony "website-render" $ do
         need webhtmlpages
 
-    pandocSiteFilter %> \out -> do
-        let source = out <.> "hs"
-        need [source]
-        cmd "stack --stack-yaml=stack-ghc8.2.yaml ghc -- -o" out source
-
     "site/_site/files/README" : [ "site/_site//*" <.> ext | ext <- webcopyfileexts ] |%> \out -> do
         let input = "site" > dropDirectory2 out
         copyFile' input out
@@ -311,12 +305,12 @@ main = do
             pageTitle = takeBaseName out
             template  = "site/site.tmpl"
             siteRoot  = if "site/_site/doc//*" ?== out then "../.." else "."
-        need [source, template, pandocSiteFilter]
+        need [source, template]
         cmd Shell pandoc "--from markdown --to html" source
                          "--template"                template
                          ("--metadata=siteRoot:"  ++ siteRoot)
                          ("--metadata=title:"     ++ pageTitle)
-                         "--filter"                  pandocSiteFilter
+                         "--lua-filter"              "tools/pandoc-site.lua"
                          "--output"                  out
 
     -- cleanup
diff --git a/tools/.gitignore b/tools/.gitignore
deleted file mode 100644
index e31cb78da..000000000
--- a/tools/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-generatetimeclock
-pandoc-site
diff --git a/tools/pandoc-site.hs b/tools/pandoc-site.hs
deleted file mode 100644
index 51909f123..000000000
--- a/tools/pandoc-site.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE CPP #-}
-
-import Text.Pandoc
-import Text.Pandoc.Walk (query)
-import Text.Pandoc.Builder (text, toList)
-import Text.Pandoc.JSON (toJSONFilter)
-import Text.Pandoc.Shared (hierarchicalize, Element(..))
-
-import Data.Maybe (fromMaybe)
-
-collectHeaders :: Block -> [Block]
-collectHeaders header@(Header _ (_, classes, _) _) =
-  if "notoc" `elem` classes
-    then []
-    else [header]
-collectHeaders _ = []
-
-markupLink :: Attr -> [Inline] -> Inline
-markupLink (headerId, _, headerProperties) headerText
-    = let linkText = fromMaybe headerText (fmap (toList . text) $ lookup "toc" headerProperties)
-       in Link nullAttr linkText (("#" ++ headerId), headerId)
-
-markupElement :: Element -> [Block]
-markupElement (Sec _ _ hAttr hText headers)
-  | headers == [] = [link]
-  | otherwise     = [link, markupElements headers]
-  where link = Plain [markupLink hAttr hText]
-markupElement n = error $    "'markupElement' should only be passed a 'Sec'\n"
-                          ++ "    saw: " ++ show n
-
-markupElements :: [Element] -> Block
-markupElements = OrderedList (1, Decimal, Period) . map markupElement
-
-createTable :: [Element] -> [Block]
-createTable [] = []
-createTable headers
-    = let navBegin  = RawBlock "html" ""
-       in [navBegin, Para [Str "Contents"], markupElements headers, navEnd]
-
-generateTOC :: [Block] -> Block -> [Block]
-generateTOC toc (Para [Str "$toc$"]) = toc
-generateTOC _   x                    = [x]
-
-tableOfContents :: Pandoc -> Pandoc
-tableOfContents (Pandoc meta blks)
-    = let headers = query collectHeaders blks
-          toc     = createTable . hierarchicalize $ headers
-       in Pandoc meta (concatMap (generateTOC toc) blks)
-
-main :: IO ()
-main = toJSONFilter tableOfContents
diff --git a/tools/pandoc-site.lua b/tools/pandoc-site.lua
new file mode 100644
index 000000000..79a008378
--- /dev/null
+++ b/tools/pandoc-site.lua
@@ -0,0 +1,59 @@
+local headers = {}
+
+function Header(h)
+  table.insert(headers, h)
+  return h
+end
+
+function isTocBlock(blk)
+  if not (blk.t               == "Para")  then return false end
+  if not  blk.content[1]                  then return false end
+  if not (blk.content[1].t    == "Str")   then return false end
+  if not (blk.content[1].text == "$toc$") then return false end
+  return true
+end
+
+function markupLink(hAttr, headerText)
+  local headerId         = hAttr.identifier
+  local headerProperties = hAttr.attributes
+  return pandoc.Link(headerText, "#" .. headerId, headerId)
+end
+
+function markupElement(elem)
+  local hAttr   = elem.attr
+  local hText   = elem.label
+  local hNested = elem.contents
+  local link    = pandoc.Plain(markupLink(hAttr, hText))
+  if not hNested[1] then return {link} end
+  return {link, markupElements(hNested)}
+end
+
+function markupElements(elems)
+  local newElems = {}
+  for _,e in pairs(elems) do
+    table.insert(newElems, markupElement(e))
+  end
+  return pandoc.OrderedList(newElems, {"1", "Decimal", "Period"})
+end
+
+function createTable(elems)
+  local navBegin  = pandoc.RawBlock("html", "")
+  local contentsP = pandoc.Para(pandoc.Str("Contents"))
+  return {navBegin, contentsP, markupElements(elems), navEnd}
+end
+
+function Pandoc(doc)
+  newBlocks = {}
+  tocBlocks = createTable(pandoc.utils.hierarchicalize(headers))
+  for _,blk in pairs(doc.blocks) do
+    if isTocBlock(blk) then
+      for _,tocBlk in pairs(tocBlocks) do
+        table.insert(newBlocks, tocBlk)
+      end
+    else
+      table.insert(newBlocks, blk)
+    end
+  end
+  return pandoc.Doc(newBlocks, doc.meta)
+end