diff --git a/AUTHORS.md b/AUTHORS.md
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -65,6 +65,7 @@
 - Cécile Chemin
 - Cédric Couralet
 - Cezary Drożak
+- Chandrahas77
 - Charanjit Singh
 - Charlotte Koch
 - Chris Black
diff --git a/MANUAL.txt b/MANUAL.txt
--- a/MANUAL.txt
+++ b/MANUAL.txt
@@ -1,7 +1,7 @@
 ---
 title: Pandoc User's Guide
 author: John MacFarlane
-date: March 17, 2024
+date: April 7, 2024
 ---
 
 # Synopsis
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,75 @@
 # Revision history for pandoc
 
+## pandoc 3.1.13 (2024-04-07)
+
+  * Org reader:
+
+    + Fix treatment of `id` property under heading (#9639).
+
+  * DocBook reader:
+
+    + Add empty title to admonition div if not present (#9569).
+      This allows admonition elements (e.g. `<note>`) to work with
+      `gfm` admonitions even if the `<title>` is not present.
+
+  * DokuWiki reader:
+
+    + Link text cannot contain formatting (e.g., `//` is not italics) (#9630).
+    + An explicitly empty link text (`[[url|]]`) works the same as an omitted
+      link text (#9632).
+
+  * Typst reader:
+
+    + Support Typst 0.11 table features: col/rowspans, table head
+      and foot (#9588).
+    + Parse cell col/rowspans.
+
+  * CSLJson writer:
+
+    + Put `$` or `$$` around math in `csljson` output (#9616).
+
+  * ConTeXt writer:
+
+    + Fix options order with `\externalfigure`. The dimensions should
+      come before the class if both are present.
+
+  * Typst writer:
+
+    + Put label after Span, not before. Labels get applied to preceding markup
+      item.
+    + Support Typst 0.11 table features (#9588): colspans, rowspans,
+      cell alignment overrides, relative column widths,
+      header and footer, multiple table bodies with intermediate headers.
+      Row heads are not yet supported.
+    + The default typst template has been modified so that tables
+      don't have lines by default. As is standard with pandoc, we only
+      add a line under a header or over a footer. However, a different
+      default stroke pattern can easily be added in a template.
+    + More reliable escaping in inline `[..]` contexts (#9586). For example,
+      we need to escape `[\1. April]` or it will be treated as an ordered list.
+    + Handle `unnumbered` on headings (#9585).
+
+  * LaTeX writer:
+
+    + Fix math inside strikeout (#9597).
+
+  * Text.Pandoc.Writers.Shared:
+
+    + Export `isOrderedListMarker` [API change].
+
+  * Change lhs tests so they don't use `--standalone`.
+    This will avoid test failures due to minor changes in
+    skylighting versions, e.g. #9589.
+
+  * Use latest texmath, typst.
+
+  * Require pandoc-lua-marshal 0.2.6 (#9613, Albert Krewinkel).
+    Fixes an issue arising when the value of `content` properties
+    on *BlockQuote*, *Figure*, and *Div* elements was an empty list.
+
+  * Update lua-filters.md (#9611, Carlos Scheidegger).
+
+
 ## pandoc 3.1.12.3 (2024-03-17)
 
   * Markdown reader: Fix bug with footnotes at end of fenced div (#9576).
@@ -32,7 +102,7 @@
   * LaTeX template: Fix block headings support for unnumbered paragraphs
     (#9542, #6018, Oliver Fabel).
 
-  * HTML templates: Replace polyfill provider (#2384, @SukkaW).
+  * HTML templates: Replace polyfill provider (#9537, @SukkaW).
     Replace polyfill.io with cdnjs.cloudflare.com/polyfill.
     polyfill.io has been acquired by Funnull, and the service has
     become unstable.
diff --git a/data/templates/default.typst b/data/templates/default.typst
--- a/data/templates/default.typst
+++ b/data/templates/default.typst
@@ -10,7 +10,8 @@
 }
 
 #set table(
-  inset: 6pt
+  inset: 6pt,
+  stroke: none
 )
 
 $if(template)$
diff --git a/pandoc.cabal b/pandoc.cabal
--- a/pandoc.cabal
+++ b/pandoc.cabal
@@ -1,6 +1,6 @@
 cabal-version:   2.4
 name:            pandoc
-version:         3.1.12.3
+version:         3.1.13
 build-type:      Simple
 license:         GPL-2.0-or-later
 license-file:    COPYING.md
@@ -326,6 +326,7 @@
                  test/tables/*.html4
                  test/tables/*.html5
                  test/tables/*.latex
+                 test/tables/*.typst
                  test/tables/*.native
                  test/tables/*.mediawiki
                  test/tables/*.jats_archiving
@@ -516,7 +517,7 @@
                  syb                   >= 0.1      && < 0.8,
                  tagsoup               >= 0.14.6   && < 0.15,
                  temporary             >= 1.1      && < 1.4,
-                 texmath               >= 0.12.8.7 && < 0.13,
+                 texmath               >= 0.12.8.8 && < 0.13,
                  text                  >= 1.1.1.0  && < 2.2,
                  text-conversions      >= 0.3      && < 0.4,
                  time                  >= 1.5      && < 1.14,
@@ -526,7 +527,7 @@
                  zip-archive           >= 0.4.3.1  && < 0.5,
                  zlib                  >= 0.5      && < 0.8,
                  xml                   >= 1.3.12   && < 1.4,
-                 typst                 >= 0.5.0.2  && < 0.5.1,
+                 typst                 >= 0.5.0.3  && < 0.5.1,
                  vector                >= 0.12     && < 0.14,
                  djot                  >= 0.1.1.3  && < 0.2,
                  tls                   >= 2.0.1    && < 2.1,
diff --git a/src/Text/Pandoc/Readers/DocBook.hs b/src/Text/Pandoc/Readers/DocBook.hs
--- a/src/Text/Pandoc/Readers/DocBook.hs
+++ b/src/Text/Pandoc/Readers/DocBook.hs
@@ -1108,8 +1108,9 @@
            -- the title: docbook references are ambiguous on that, and some implementations of admonitions
            -- (e.g. asciidoctor) just use an icon in all cases. To be conservative, we don't
            -- include the label and leave it to styling.
-           title <- case filterChild (named "title") e of
-                        Just t  -> divWith ("", ["title"], []) . plain <$> getInlines t
+           title <- divWith ("", ["title"], []) . plain <$>
+                    case filterChild (named "title") e of
+                        Just t  -> getInlines t
                         Nothing -> return mempty
            -- this will ignore the title element if it is present
            b <- getBlocks e
diff --git a/src/Text/Pandoc/Readers/DokuWiki.hs b/src/Text/Pandoc/Readers/DokuWiki.hs
--- a/src/Text/Pandoc/Readers/DokuWiki.hs
+++ b/src/Text/Pandoc/Readers/DokuWiki.hs
@@ -300,8 +300,11 @@
 parseLink f l r = f
   <$  textStr l
   <*> many1TillChar anyChar (lookAhead (void (char '|') <|> try (void $ textStr r)))
-  <*> optionMaybe (B.trimInlines . mconcat <$> (char '|' *> manyTill inline (try $ lookAhead $ textStr r)))
-  <*  textStr r
+  <*> ( (char '|' *> optionMaybe (B.trimInlines . B.text . T.pack <$>
+                       many1Till anyChar (lookAhead (try (textStr r)))))
+       <|> pure Nothing
+      )
+  <* textStr r
 
 -- | Split Interwiki link into left and right part
 -- | Return Nothing if it is not Interwiki link
diff --git a/src/Text/Pandoc/Readers/Org/DocumentTree.hs b/src/Text/Pandoc/Readers/Org/DocumentTree.hs
--- a/src/Text/Pandoc/Readers/Org/DocumentTree.hs
+++ b/src/Text/Pandoc/Readers/Org/DocumentTree.hs
@@ -16,7 +16,7 @@
   ) where
 
 import Control.Arrow ((***), first)
-import Control.Monad (guard)
+import Control.Monad (guard, mplus)
 import Data.List (intersperse)
 import Data.Maybe (mapMaybe)
 import Data.Text (Text)
@@ -317,10 +317,12 @@
   let
     toTextPair = fromKey *** fromValue
     customIdKey = toPropertyKey "custom_id"
+    idKey = toPropertyKey "id"
     classKey    = toPropertyKey "class"
     unnumberedKey = toPropertyKey "unnumbered"
-    specialProperties = [customIdKey, classKey, unnumberedKey]
-    id'  = maybe mempty fromValue . lookup customIdKey $ properties
+    specialProperties = [customIdKey, idKey, classKey, unnumberedKey]
+    id'  = maybe mempty fromValue $
+              (lookup customIdKey properties `mplus` lookup idKey properties)
     cls  = maybe mempty fromValue . lookup classKey    $ properties
     kvs' = map toTextPair . filter ((`notElem` specialProperties) . fst)
            $ properties
diff --git a/src/Text/Pandoc/Readers/Typst.hs b/src/Text/Pandoc/Readers/Typst.hs
--- a/src/Text/Pandoc/Readers/Typst.hs
+++ b/src/Text/Pandoc/Readers/Typst.hs
@@ -30,7 +30,7 @@
 import Text.Pandoc.Error (PandocError(..))
 import Text.Pandoc.Shared (tshow, blocksToInlines)
 import Control.Monad.Except (throwError)
-import Control.Monad (MonadPlus (mplus), void, mzero, guard)
+import Control.Monad (MonadPlus (mplus), void, guard, foldM)
 import qualified Data.Foldable as F
 import qualified Data.Map as M
 import Data.Maybe (catMaybes, fromMaybe)
@@ -39,17 +39,17 @@
 import qualified Data.Set as Set
 import Data.Text (Text)
 import qualified Data.Text as T
-import qualified Data.Vector as V
 import qualified Text.Pandoc.Builder as B
 import Text.Pandoc.Walk
 import Text.Parsec
 import Text.TeXMath (writeTeX)
 import Text.TeXMath.Shared (getSpaceChars)
 import Text.Pandoc.Readers.Typst.Math (pMathMany)
-import Text.Pandoc.Readers.Typst.Parsing (pTok, ignored, chunks, getField, P,
+import Text.Pandoc.Readers.Typst.Parsing (pTok, ignored, getField, P,
                                           PState(..), defaultPState)
 import Typst.Methods (formatNumber, applyPureFunction)
 import Typst.Types
+import qualified Data.Vector as V
 
 -- import Debug.Trace
 
@@ -330,78 +330,8 @@
         B.divWith ("", [], [("stack", repr (VDirection dir))]) $
           mconcat $
             map (B.divWith ("", [], [])) children)
-  ,("grid", \mbident fields -> do
-      children <- getField "children" fields >>= mapM (pWithContents pBlocks) . V.toList
-      (columns :: Val) <- getField "columns" fields
-      let toWidth (VFraction f) = Just (floor $ 1000 * f)
-          toWidth _ = Nothing
-      let normalizeWidths xs =
-            let givenwidths = catMaybes xs
-                (totgivenwidth :: Int) = sum givenwidths
-                avgwidth = totgivenwidth `div` length givenwidths
-                totwidth = avgwidth * length xs
-             in if null givenwidths
-                  then replicate (length xs) B.ColWidthDefault
-                  else
-                    map
-                      ( \case
-                          Just x -> B.ColWidth (fromIntegral x / fromIntegral totwidth)
-                          Nothing ->
-                            B.ColWidth (fromIntegral avgwidth / fromIntegral totwidth)
-                      )
-                      xs
-      widths <- case columns of
-        VInteger x -> pure $ replicate (fromIntegral x) B.ColWidthDefault
-        VArray x -> pure $ normalizeWidths $ map toWidth (V.toList x)
-        VNone -> pure [B.ColWidthDefault]
-        _ -> fail $ "Could not determine number of columns: " <> show columns
-      let numcols = length widths
-      align <- getField "align" fields
-      let toAlign (VAlignment (Just horiz) _) =
-            case horiz of
-              HorizStart -> B.AlignLeft
-              HorizLeft -> B.AlignLeft
-              HorizEnd -> B.AlignRight
-              HorizRight -> B.AlignRight
-              HorizCenter -> B.AlignCenter
-          toAlign _ = B.AlignDefault
-      aligns <-
-        case align of
-          VAlignment {} -> pure $ replicate numcols (toAlign align)
-          VArray v -> pure $ map toAlign (V.toList v)
-          VFunction _ _ f -> do
-            mapM
-              ( \colnum -> case applyPureFunction
-                  f
-                  [VInteger colnum, VInteger 0] of
-                  Success x -> pure $ toAlign x
-                  Failure e -> fail e
-              )
-              [0 .. (fromIntegral numcols - 1)]
-          _ -> pure $ replicate numcols B.AlignDefault
-      let colspecs = zip (aligns ++ repeat B.AlignDefault) widths
-      let rows =
-            map (B.Row B.nullAttr) $
-              chunks numcols $
-                map
-                  ( B.Cell
-                      B.nullAttr
-                      B.AlignDefault
-                      (B.RowSpan 1)
-                      (B.ColSpan 1)
-                      . B.toList
-                  )
-                  children
-      pure $
-        B.tableWith
-          (fromMaybe "" mbident, [], [])
-          (B.Caption mempty mempty)
-          colspecs
-          (B.TableHead B.nullAttr [])
-          [B.TableBody B.nullAttr 0 [] rows]
-          (B.TableFoot B.nullAttr []))
-  ,("table", \mbident fields ->
-       maybe mzero (\f -> f mbident fields) $ M.lookup "grid" blockHandlers)
+  ,("grid", \mbident fields -> parseTable mbident fields)
+  ,("table", \mbident fields -> parseTable mbident fields)
   ,("figure", \mbident fields -> do
       body <- getField "body" fields >>= pWithContents pBlocks
       (mbCaption :: Maybe (Seq Content)) <- getField "caption" fields
@@ -630,3 +560,135 @@
    go (Elt{ eltFields = fs }) = \ts -> foldr go' ts fs
    go' (VContent cs) = (findLabels cs ++)
    go' _ = id
+
+parseTable :: PandocMonad m
+           => Maybe Text -> M.Map Identifier Val -> P m B.Blocks
+parseTable mbident fields = do
+  children <- V.toList <$> getField "children" fields
+  (columns :: Val) <- getField "columns" fields
+  let toWidth (VFraction f) = Just (floor $ 1000 * f)
+      toWidth _ = Nothing
+  let normalizeWidths xs =
+        let givenwidths = catMaybes xs
+            (totgivenwidth :: Int) = sum givenwidths
+            avgwidth = totgivenwidth `div` length givenwidths
+            totwidth = avgwidth * length xs
+         in if null givenwidths
+              then replicate (length xs) B.ColWidthDefault
+              else
+                map
+                  ( \case
+                      Just x ->
+                        B.ColWidth (fromIntegral x / fromIntegral totwidth)
+                      Nothing ->
+                        B.ColWidth
+                        (fromIntegral avgwidth / fromIntegral totwidth)
+                  )
+                  xs
+  widths <- case columns of
+    VInteger x -> pure $ replicate (fromIntegral x) B.ColWidthDefault
+    VArray x -> pure $ normalizeWidths $ map toWidth (V.toList x)
+    VNone -> pure [B.ColWidthDefault]
+    _ -> fail $ "Could not determine number of columns: " <> show columns
+  let numcols = length widths
+  align <- getField "align" fields
+  let toAlign (VAlignment (Just horiz) _) =
+        case horiz of
+          HorizStart -> B.AlignLeft
+          HorizLeft -> B.AlignLeft
+          HorizEnd -> B.AlignRight
+          HorizRight -> B.AlignRight
+          HorizCenter -> B.AlignCenter
+      toAlign _ = B.AlignDefault
+  aligns <-
+    case align of
+      VAlignment {} -> pure $ replicate numcols (toAlign align)
+      VArray v -> pure $ map toAlign (V.toList v)
+      VFunction _ _ f -> do
+        mapM
+          ( \colnum -> case applyPureFunction
+              f
+              [VInteger colnum, VInteger 0] of
+              Success x -> pure $ toAlign x
+              Failure e -> fail e
+          )
+          [0 .. (fromIntegral numcols - 1)]
+      _ -> pure $ replicate numcols B.AlignDefault
+  let colspecs = zip (aligns ++ repeat B.AlignDefault) widths
+  let addCell' cell Nothing = addCell' cell (Just ([], []))
+      addCell' cell@(B.Cell _ _ (B.RowSpan rowspan) (B.ColSpan colspan) _)
+       (Just (freecols, revrows))  =
+        let freecols' =
+              case (rowspan + 1) - length freecols of
+                n | n < 0 -> freecols
+                  | otherwise -> freecols ++ replicate n numcols
+        in case freecols' of
+             [] -> -- should not happen
+                   error "empty freecols'"
+             x:xs
+               | colspan <= x -- there is room on current row
+                 -> let (as, bs) = splitAt rowspan (x:xs)
+                    in  Just
+                        (map (\z -> z - colspan) as ++ bs,
+                           case revrows of
+                             [] -> [[cell]]
+                             r:rs -> (cell:r):rs)
+               | otherwise ->
+                    let (as, bs) = splitAt rowspan xs
+                    in  Just (map (\z -> z - colspan) as ++ bs, [cell]:revrows)
+  let addCell tableSection cell (TableData tdata) =
+        TableData (M.alter (addCell' cell) tableSection tdata)
+  let toCell tableSection tableData contents = do
+        case contents of
+          [Elt (Identifier "grid.cell") _pos fs] -> do
+            bs <- B.toList <$> (getField "body" fs >>= pWithContents pBlocks)
+            rowspan <- getField "rowspan" fs <|> pure 1
+            colspan <- getField "colspan" fs <|> pure 1
+            align' <- (toAlign <$> getField "align" fs) <|> pure B.AlignDefault
+            pure $ addCell tableSection
+              (B.Cell B.nullAttr align' (B.RowSpan rowspan)
+                (B.ColSpan colspan) bs) tableData
+          [Elt (Identifier "table.cell") pos fs] ->
+            toCell tableSection tableData [Elt (Identifier "grid.cell") pos fs]
+          [Elt (Identifier "table.vline") _pos _fs] -> pure tableData
+          [Elt (Identifier "table.hline") _pos _fs] -> pure tableData
+          [Elt (Identifier "grid.vline") _pos _fs] -> pure tableData
+          [Elt (Identifier "grid.hline") _pos _fs] -> pure tableData
+          [Elt (Identifier "table.header") _pos fs] ->
+            getField "children" fs >>=
+              foldM (toCell THeader) tableData . V.toList
+          [Elt (Identifier "table.footer") _pos fs] ->
+            getField "children" fs >>=
+              foldM (toCell TFooter) tableData . V.toList
+          _ -> do
+            bs <- B.toList <$> pWithContents pBlocks contents
+            pure $ addCell tableSection
+              (B.Cell B.nullAttr B.AlignDefault (B.RowSpan 1) (B.ColSpan 1) bs)
+              tableData
+  tableData <- foldM (toCell TBody) (TableData mempty) children
+  let getRows tablePart = map (B.Row B.nullAttr . reverse)
+                          . maybe [] (reverse . snd)
+                          . M.lookup tablePart . unTableData
+  let headRows = getRows THeader tableData
+  let bodyRows = getRows TBody tableData
+  let footRows = getRows TFooter tableData
+  pure $
+    B.tableWith
+      (fromMaybe "" mbident, [], [])
+      (B.Caption mempty mempty)
+      colspecs
+      (B.TableHead B.nullAttr headRows)
+      [B.TableBody B.nullAttr 0 [] bodyRows]
+      (B.TableFoot B.nullAttr footRows)
+
+data TableSection = THeader | TBody | TFooter
+  deriving (Show, Ord, Eq)
+
+newtype TableData =
+  TableData { unTableData :: M.Map TableSection ([Int], [[Cell]]) }
+  deriving (Show)
+  -- for each table section, we have a pair
+  -- the first element indicates the number of column spaces left
+  -- in [currentLine, nextLine, lineAfter, etc.]
+  -- the second element is a list of rows, in reverse order,
+  -- each of which is a list of cells, in reverse order
diff --git a/src/Text/Pandoc/Writers/ConTeXt.hs b/src/Text/Pandoc/Writers/ConTeXt.hs
--- a/src/Text/Pandoc/Writers/ConTeXt.hs
+++ b/src/Text/Pandoc/Writers/ConTeXt.hs
@@ -707,7 +707,7 @@
              if isURI src
                 then src
                 else T.pack $ unEscapeString $ T.unpack src
-  return $ braces $ "\\externalfigure" <> brackets (literal src') <> dims <> clas
+  return $ braces $ "\\externalfigure" <> brackets (literal src') <> clas <> dims
 inlineToConTeXt (Note contents) = do
   contents' <- blockListToConTeXt contents
   let codeBlock x@(CodeBlock _ _) = [x]
diff --git a/src/Text/Pandoc/Writers/CslJson.hs b/src/Text/Pandoc/Writers/CslJson.hs
--- a/src/Text/Pandoc/Writers/CslJson.hs
+++ b/src/Text/Pandoc/Writers/CslJson.hs
@@ -71,7 +71,8 @@
 fromInline Space = CslText " "
 fromInline SoftBreak = CslText " "
 fromInline LineBreak = CslText "\n"
-fromInline (Math _ t) = CslText t
+fromInline (Math InlineMath t) = CslText $ "$" <> t <> "$"
+fromInline (Math DisplayMath t) = CslText $ "$$" <> t <> "$$"
 fromInline (RawInline _ _) = CslEmpty
 fromInline (Link _ ils _) = fromInlines ils
 fromInline (Image _ ils _) = fromInlines ils
diff --git a/src/Text/Pandoc/Writers/LaTeX.hs b/src/Text/Pandoc/Writers/LaTeX.hs
--- a/src/Text/Pandoc/Writers/LaTeX.hs
+++ b/src/Text/Pandoc/Writers/LaTeX.hs
@@ -868,7 +868,10 @@
   -- incorrect results if there is a space, see #5529
   contents <- inlineListToLaTeX $ walk (concatMap protectCode) lst
   modify $ \s -> s{ stStrikeout = True }
-  return $ inCmd "st" contents
+  -- soul doesn't like \(..\) delimiters, so we change these to $ (#9597):
+  let fixMath = T.replace "\\(" "$" . T.replace "\\)" "$" .
+                T.replace "\\[" "$$" . T.replace "\\]" "$$"
+  return $ inCmd "st" $ fixMath <$> contents
 inlineToLaTeX (Superscript lst) =
   inCmd "textsuperscript" <$> inlineListToLaTeX lst
 inlineToLaTeX (Subscript lst) =
diff --git a/src/Text/Pandoc/Writers/Markdown/Inline.hs b/src/Text/Pandoc/Writers/Markdown/Inline.hs
--- a/src/Text/Pandoc/Writers/Markdown/Inline.hs
+++ b/src/Text/Pandoc/Writers/Markdown/Inline.hs
@@ -332,14 +332,6 @@
   toList (Concat a b) = a : toList b
   toList x = [x]
 
-isOrderedListMarker :: Text -> Bool
-isOrderedListMarker xs = not (T.null xs) && (T.last xs `elem` ['.',')']) &&
-              isRight (runParser (anyOrderedListMarker >> eof)
-                       defaultParserState "" xs)
- where
-  isRight (Right _) = True
-  isRight (Left  _) = False
-
 -- | Convert Pandoc inline element to markdown.
 inlineToMarkdown :: PandocMonad m => WriterOptions -> Inline -> MD m (Doc Text)
 inlineToMarkdown opts (Span ("",["emoji"],kvs) [Str s]) =
diff --git a/src/Text/Pandoc/Writers/Shared.hs b/src/Text/Pandoc/Writers/Shared.hs
--- a/src/Text/Pandoc/Writers/Shared.hs
+++ b/src/Text/Pandoc/Writers/Shared.hs
@@ -44,11 +44,13 @@
                      , splitSentences
                      , ensureValidXmlIdentifiers
                      , setupTranslations
+                     , isOrderedListMarker
                      )
 where
 import Safe (lastMay)
 import qualified Data.ByteString.Lazy as BL
 import Control.Monad (zipWithM)
+import Data.Either (isRight)
 import Data.Aeson (ToJSON (..), encode)
 import Data.Char (chr, ord, isSpace, isLetter, isUpper)
 import Data.List (groupBy, intersperse, transpose, foldl')
@@ -61,6 +63,8 @@
 import Text.Pandoc.CSS (cssAttributes)
 import Text.Pandoc.Definition
 import Text.Pandoc.Options
+import Text.Pandoc.Parsing (runParser, eof, defaultParserState,
+                            anyOrderedListMarker)
 import Text.DocLayout
 import Text.Pandoc.Shared (stringify, makeSections, blocksToInlines)
 import Text.Pandoc.Walk (Walkable(..))
@@ -626,3 +630,9 @@
             "" -> pure defLang
             s  -> fromMaybe defLang <$> toLang (Just s)
   setTranslations lang
+
+-- True if the string would count as a Markdown ordered list marker.
+isOrderedListMarker :: Text -> Bool
+isOrderedListMarker xs = not (T.null xs) && (T.last xs `elem` ['.',')']) &&
+              isRight (runParser (anyOrderedListMarker >> eof)
+                       defaultParserState "" xs)
diff --git a/src/Text/Pandoc/Writers/Typst.hs b/src/Text/Pandoc/Writers/Typst.hs
--- a/src/Text/Pandoc/Writers/Typst.hs
+++ b/src/Text/Pandoc/Writers/Typst.hs
@@ -20,12 +20,13 @@
 import Text.Pandoc.Class ( PandocMonad)
 import Text.Pandoc.Options ( WriterOptions(..), WrapOption(..), isEnabled )
 import Data.Text (Text)
-import Data.List (intercalate)
+import Data.List (intercalate, intersperse)
 import Network.URI (unEscapeString)
 import qualified Data.Text as T
 import Control.Monad.State ( StateT, evalStateT, gets, modify )
 import Text.Pandoc.Writers.Shared ( metaToContext, defField, resetField,
-                                    toLegacyTable, lookupMetaString )
+                                    lookupMetaString,
+                                    isOrderedListMarker )
 import Text.Pandoc.Shared (isTightList, orderedListMarkers, tshow)
 import Text.Pandoc.Writers.Math (convertMath)
 import qualified Text.TeXMath as TM
@@ -33,6 +34,7 @@
 import Text.DocTemplates (renderTemplate)
 import Text.Pandoc.Extensions (Extension(..))
 import Text.Collate.Lang (Lang(..), parseLang)
+import Text.Printf (printf)
 import Data.Char (isAlphaNum)
 
 -- | Convert Pandoc to Typst.
@@ -97,13 +99,18 @@
     Header level (ident,cls,_) inlines -> do
       contents <- inlinesToTypst inlines
       let lab = toLabel FreestandingLabel ident
+      let headingAttrs =
+            ["outlined: false" | "unlisted" `elem` cls] ++
+            ["numbering: none" | "unnumbered" `elem` cls]
       return $
-        if "unlisted" `elem` cls
-           then literal "#heading(outlined: false)" <> brackets contents <>
-                 cr <> lab
-           else nowrap
+        if null headingAttrs
+           then nowrap
                  (literal (T.replicate level "=") <> space <> contents) <>
                  cr <> lab
+           else literal "#heading" <>
+                  parens (literal (T.intercalate ", "
+                              ("level: " <> tshow level : headingAttrs))) <>
+                  brackets contents <> cr <> lab
     RawBlock fmt str ->
       case fmt of
         Format "typst" -> return $ literal str
@@ -156,33 +163,80 @@
                    else vsep items') $$ blankline
     DefinitionList items ->
       ($$ blankline) . vsep <$> mapM defListItemToTypst items
-    Table (ident,_,_) blkCapt colspecs thead tbodies tfoot -> do
-      let (caption, aligns, _, headers, rows) =
-            toLegacyTable blkCapt colspecs thead tbodies tfoot
-      let numcols = length aligns
-      headers' <- mapM blocksToTypst headers
-      rows' <- mapM (mapM blocksToTypst) rows
+    Table (ident,_,_) (Caption _ caption) colspecs thead tbodies tfoot -> do
+      let lab = toLabel FreestandingLabel ident
       capt' <- if null caption
                   then return mempty
                   else do
-                    captcontents <- inlinesToTypst caption
+                    captcontents <- blocksToTypst caption
                     return $ ", caption: " <> brackets captcontents
-      let lab = toLabel FreestandingLabel ident
+      let numcols = length colspecs
+      let (aligns, widths) = unzip colspecs
+      let commaSep = hcat . intersperse ", "
+      let toPercentage (ColWidth w) =
+            literal $ (T.dropWhileEnd (== '.') . T.dropWhileEnd (== '0'))
+                         (T.pack (printf "%0.2f" (w * 100))) <> "%"
+          toPercentage ColWidthDefault = literal "auto"
+      let columns = if all (== ColWidthDefault) widths
+                       then literal $ tshow numcols
+                       else parens (commaSep (map toPercentage widths))
       let formatalign AlignLeft = "left,"
           formatalign AlignRight = "right,"
           formatalign AlignCenter = "center,"
           formatalign AlignDefault = "auto,"
       let alignarray = parens $ mconcat $ map formatalign aligns
+      let fromCell (Cell _attr alignment rowspan colspan bs) = do
+            let cellattrs =
+                  (case alignment of
+                     AlignDefault -> []
+                     AlignLeft -> [ "align: left" ]
+                     AlignRight -> [ "align: right" ]
+                     AlignCenter -> [ "align: center" ]) ++
+                  (case rowspan of
+                     RowSpan 1 -> []
+                     RowSpan n -> [ "rowspan: " <> tshow n ]) ++
+                  (case colspan of
+                     ColSpan 1 -> []
+                     ColSpan n -> [ "colspan: " <> tshow n ])
+            cellContents <- blocksToTypst bs
+            pure $ if null cellattrs
+                      then brackets cellContents
+                      else "table.cell" <>
+                            parens
+                             (literal (T.intercalate ", " cellattrs)) <>
+                            brackets cellContents
+      let fromRow (Row _ cs) =
+            (<> ",") . commaSep <$> mapM fromCell cs
+      let fromHead (TableHead _attr headRows) =
+            if null headRows
+               then pure mempty
+               else (($$ "table.hline(),") .
+                      (<> ",") . ("table.header" <>) . parens . nest 2 . vcat)
+                      <$> mapM fromRow headRows
+      let fromFoot (TableFoot _attr footRows) =
+            if null footRows
+               then pure mempty
+               else (("table.hline()," $$) .
+                      (<> ",") . ("table.footer" <>) . parens . nest 2 . vcat)
+                      <$> mapM fromRow footRows
+      let fromTableBody (TableBody _attr _rowHeadCols headRows bodyRows) = do
+            hrows <- mapM fromRow headRows
+            brows <- mapM fromRow bodyRows
+            pure $ vcat (hrows ++ ["table.hline()," | not (null hrows)] ++ brows)
+      header <- fromHead thead
+      footer <- fromFoot tfoot
+      body <- vcat <$> mapM fromTableBody tbodies
       return $
         "#figure("
         $$
         nest 2
          ("align(center)[#table("
           $$ nest 2
-             (  "columns: " <> text (show numcols) <> "," -- auto
-             $$ "align: (col, row) => " <> alignarray <> ".at(col),"
-             $$ hsep (map ((<>",") . brackets) headers')
-             $$ vcat (map (\x -> brackets x <> ",") (concat rows'))
+             (  "columns: " <> columns <> ","
+             $$ "align: " <> alignarray <> ","
+             $$ header
+             $$ body
+             $$ footer
              )
           $$ ")]"
           $$ capt'
@@ -227,9 +281,6 @@
   return $ hang ind (marker <> space) contents
 
 inlinesToTypst :: PandocMonad m => [Inline] -> TW m (Doc Text)
-inlinesToTypst ils@(Str t : _) -- need to escape - in '[-]' #9478
-  | Just (c, _) <- T.uncons t
-  , needsEscapeAtLineStart c = ("\\" <>) . hcat <$> mapM inlineToTypst ils
 inlinesToTypst ils = hcat <$> mapM inlineToTypst ils
 
 inlineToTypst :: PandocMonad m => Inline -> TW m (Doc Text)
@@ -274,7 +325,7 @@
     SmallCaps inlines -> textstyle "#smallcaps" inlines
     Span (ident,_,_) inlines -> do
       let lab = toLabel FreestandingLabel ident
-      (lab $$) <$> inlinesToTypst inlines
+      (<> lab) <$> inlinesToTypst inlines
     Quoted quoteType inlines -> do
       let q = case quoteType of
                    DoubleQuote -> literal "\""
@@ -325,7 +376,15 @@
 
 textstyle :: PandocMonad m => Doc Text -> [Inline] -> TW m (Doc Text)
 textstyle s inlines =
-  (<> endCode) . (s <>) . brackets <$> inlinesToTypst inlines
+  (<> endCode) . (s <>) . brackets . addEscape <$> inlinesToTypst inlines
+ where
+   addEscape =
+     case inlines of
+       (Str t : _)
+         | isOrderedListMarker t -> ("\\" <>)
+         | Just (c, _) <- T.uncons t
+         , needsEscapeAtLineStart c -> ("\\" <>)
+       _ -> id
 
 escapeTypst :: EscapeContext -> Text -> Doc Text
 escapeTypst context t =
diff --git a/test/Tests/Old.hs b/test/Tests/Old.hs
--- a/test/Tests/Old.hs
+++ b/test/Tests/Old.hs
@@ -199,7 +199,7 @@
     [ testGroup "writer" $ writerTests' "ms"
     ]
   , testGroup "typst"
-    [ testGroup "writer" $ writerTests' "typst"
+    [ testGroup "writer" $ writerTests' "typst" ++ extWriterTests' "typst"
     , testGroup "reader"
        [ test' "typst-reader" ["-r", "typst", "-w", "native", "-s"]
           "typst-reader.typ" "typst-reader.native"
@@ -264,7 +264,7 @@
     ]
   where
     t n f = test pandocPath
-             n ["--wrap=preserve", "-r", "native", "-s",
+             n ["--wrap=preserve", "-r", "native",
               "--markdown-headings=setext", "-w", f]
              "lhs-test.native" ("lhs-test" <.> f)
 
diff --git a/test/command/9569.md b/test/command/9569.md
new file mode 100644
--- /dev/null
+++ b/test/command/9569.md
@@ -0,0 +1,19 @@
+```
+% pandoc -f docbook -t gfm
+<note>
+  <para>Some content</para>
+</note>
+<para>Spacer.</para>
+<note>
+  <title>A note with a title. Works, but the title is discarded.</title>
+  <para>Some content</para>
+</note>
+^D
+> [!NOTE]
+> Some content
+
+Spacer.
+
+> [!NOTE]
+> Some content
+```
diff --git a/test/command/9585.md b/test/command/9585.md
new file mode 100644
--- /dev/null
+++ b/test/command/9585.md
@@ -0,0 +1,11 @@
+```
+% pandoc -f native -t typst
+[ Header 2 ( "" , [] , [] ) [ Str "One" ]
+, Header 2 ( "" , [ "unnumbered", "unlisted" ] , [] ) [ Str "Two" ]
+, Header 2 ( "" , [] , [] ) [ Str "Three" ]
+]
+^D
+== One
+#heading(level: 2, outlined: false, numbering: none)[Two]
+== Three
+```
diff --git a/test/command/9586.md b/test/command/9586.md
new file mode 100644
--- /dev/null
+++ b/test/command/9586.md
@@ -0,0 +1,7 @@
+```
+% pandoc -t typst
+**1. April 2024**
+^D
+#strong[\1. April 2024]
+
+```
diff --git a/test/command/9597.md b/test/command/9597.md
new file mode 100644
--- /dev/null
+++ b/test/command/9597.md
@@ -0,0 +1,6 @@
+```
+% pandoc -t latex
+~~$T$~~
+^D
+\st{$T$}
+```
diff --git a/test/command/9616.md b/test/command/9616.md
new file mode 100644
--- /dev/null
+++ b/test/command/9616.md
@@ -0,0 +1,16 @@
+```
+% pandoc -f bibtex -t csljson -s
+@MISC {3252037,
+    TITLE = {Isomorphism between projective varieties $\mathbf{P}^{1}$ and a conic in $\mathbf{P}^{2}$},
+    URL = {https://math.stackexchange.com/q/3252037}
+}
+^D
+[
+  {
+    "URL": "https://math.stackexchange.com/q/3252037",
+    "id": "3252037",
+    "title": "Isomorphism between projective varieties $\\mathbf{P}^{1}$ and a conic in $\\mathbf{P}^{2}$",
+    "type": ""
+  }
+]
+```
diff --git a/test/command/9630.md b/test/command/9630.md
new file mode 100644
--- /dev/null
+++ b/test/command/9630.md
@@ -0,0 +1,12 @@
+```
+% pandoc -f dokuwiki
+  * [[https://example.com|https://example.com]]
+  * [[https://example.com/start#monitoring|https://example.com/start#monitoring]]
+^D
+<ul>
+<li><a href="https://example.com">https://example.com</a></li>
+<li><a
+href="https://example.com/start#monitoring">https://example.com/start#monitoring</a></li>
+</ul>
+
+```
diff --git a/test/command/9632.md b/test/command/9632.md
new file mode 100644
--- /dev/null
+++ b/test/command/9632.md
@@ -0,0 +1,7 @@
+```
+% pandoc -f dokuwiki
+[[.:supprimer-profil-windows|]]
+^D
+<p><a href="supprimer-profil-windows">supprimer-profil-windows</a></p>
+
+```
diff --git a/test/command/9639.md b/test/command/9639.md
new file mode 100644
--- /dev/null
+++ b/test/command/9639.md
@@ -0,0 +1,11 @@
+```
+% pandoc -f org -t native
+#+title: repro
+
+* heading-name
+:PROPERTIES:
+:ID:       123abc
+:END:
+^D
+[ Header 1 ( "123abc" , [] , [] ) [ Str "heading-name" ] ]
+```
diff --git a/test/lhs-test.html b/test/lhs-test.html
--- a/test/lhs-test.html
+++ b/test/lhs-test.html
@@ -1,238 +1,3 @@
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
-<head>
-  <meta charset="utf-8" />
-  <meta name="generator" content="pandoc" />
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
-  <title>lhs-test</title>
-  <style>
-    html {
-      color: #1a1a1a;
-      background-color: #fdfdfd;
-    }
-    body {
-      margin: 0 auto;
-      max-width: 36em;
-      padding-left: 50px;
-      padding-right: 50px;
-      padding-top: 50px;
-      padding-bottom: 50px;
-      hyphens: auto;
-      overflow-wrap: break-word;
-      text-rendering: optimizeLegibility;
-      font-kerning: normal;
-    }
-    @media (max-width: 600px) {
-      body {
-        font-size: 0.9em;
-        padding: 12px;
-      }
-      h1 {
-        font-size: 1.8em;
-      }
-    }
-    @media print {
-      html {
-        background-color: white;
-      }
-      body {
-        background-color: transparent;
-        color: black;
-        font-size: 12pt;
-      }
-      p, h2, h3 {
-        orphans: 3;
-        widows: 3;
-      }
-      h2, h3, h4 {
-        page-break-after: avoid;
-      }
-    }
-    p {
-      margin: 1em 0;
-    }
-    a {
-      color: #1a1a1a;
-    }
-    a:visited {
-      color: #1a1a1a;
-    }
-    img {
-      max-width: 100%;
-    }
-    svg {
-      height: auto;
-      max-width: 100%;
-    }
-    h1, h2, h3, h4, h5, h6 {
-      margin-top: 1.4em;
-    }
-    h5, h6 {
-      font-size: 1em;
-      font-style: italic;
-    }
-    h6 {
-      font-weight: normal;
-    }
-    ol, ul {
-      padding-left: 1.7em;
-      margin-top: 1em;
-    }
-    li > ol, li > ul {
-      margin-top: 0;
-    }
-    blockquote {
-      margin: 1em 0 1em 1.7em;
-      padding-left: 1em;
-      border-left: 2px solid #e6e6e6;
-      color: #606060;
-    }
-    code {
-      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
-      font-size: 85%;
-      margin: 0;
-      hyphens: manual;
-    }
-    pre {
-      margin: 1em 0;
-      overflow: auto;
-    }
-    pre code {
-      padding: 0;
-      overflow: visible;
-      overflow-wrap: normal;
-    }
-    .sourceCode {
-     background-color: transparent;
-     overflow: visible;
-    }
-    hr {
-      background-color: #1a1a1a;
-      border: none;
-      height: 1px;
-      margin: 1em 0;
-    }
-    table {
-      margin: 1em 0;
-      border-collapse: collapse;
-      width: 100%;
-      overflow-x: auto;
-      display: block;
-      font-variant-numeric: lining-nums tabular-nums;
-    }
-    table caption {
-      margin-bottom: 0.75em;
-    }
-    tbody {
-      margin-top: 0.5em;
-      border-top: 1px solid #1a1a1a;
-      border-bottom: 1px solid #1a1a1a;
-    }
-    th {
-      border-top: 1px solid #1a1a1a;
-      padding: 0.25em 0.5em 0.25em 0.5em;
-    }
-    td {
-      padding: 0.125em 0.5em 0.25em 0.5em;
-    }
-    header {
-      margin-bottom: 4em;
-      text-align: center;
-    }
-    #TOC li {
-      list-style: none;
-    }
-    #TOC ul {
-      padding-left: 1.3em;
-    }
-    #TOC > ul {
-      padding-left: 0;
-    }
-    #TOC a:not(:hover) {
-      text-decoration: none;
-    }
-    code{white-space: pre-wrap;}
-    span.smallcaps{font-variant: small-caps;}
-    div.columns{display: flex; gap: min(4vw, 1.5em);}
-    div.column{flex: auto; overflow-x: auto;}
-    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
-    /* The extra [class] is a hack that increases specificity enough to
-       override a similar rule in reveal.js */
-    ul.task-list[class]{list-style: none;}
-    ul.task-list li input[type="checkbox"] {
-      font-size: inherit;
-      width: 0.8em;
-      margin: 0 0.8em 0.2em -1.6em;
-      vertical-align: middle;
-    }
-    .display.math{display: block; text-align: center; margin: 0.5rem auto;}
-    /* CSS for syntax highlighting */
-    pre > code.sourceCode { white-space: pre; position: relative; }
-    pre > code.sourceCode > span { line-height: 1.25; }
-    pre > code.sourceCode > span:empty { height: 1.2em; }
-    .sourceCode { overflow: visible; }
-    code.sourceCode > span { color: inherit; text-decoration: inherit; }
-    div.sourceCode { margin: 1em 0; }
-    pre.sourceCode { margin: 0; }
-    @media screen {
-    div.sourceCode { overflow: auto; }
-    }
-    @media print {
-    pre > code.sourceCode { white-space: pre-wrap; }
-    pre > code.sourceCode > span { display: inline-block; text-indent: -5em; padding-left: 5em; }
-    }
-    pre.numberSource code
-      { counter-reset: source-line 0; }
-    pre.numberSource code > span
-      { position: relative; left: -4em; counter-increment: source-line; }
-    pre.numberSource code > span > a:first-child::before
-      { content: counter(source-line);
-        position: relative; left: -1em; text-align: right; vertical-align: baseline;
-        border: none; display: inline-block;
-        -webkit-touch-callout: none; -webkit-user-select: none;
-        -khtml-user-select: none; -moz-user-select: none;
-        -ms-user-select: none; user-select: none;
-        padding: 0 4px; width: 4em;
-        color: #aaaaaa;
-      }
-    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
-    div.sourceCode
-      {   }
-    @media screen {
-    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
-    }
-    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
-    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
-    code span.at { color: #7d9029; } /* Attribute */
-    code span.bn { color: #40a070; } /* BaseN */
-    code span.bu { color: #008000; } /* BuiltIn */
-    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
-    code span.ch { color: #4070a0; } /* Char */
-    code span.cn { color: #880000; } /* Constant */
-    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
-    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
-    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
-    code span.dt { color: #902000; } /* DataType */
-    code span.dv { color: #40a070; } /* DecVal */
-    code span.er { color: #ff0000; font-weight: bold; } /* Error */
-    code span.ex { } /* Extension */
-    code span.fl { color: #40a070; } /* Float */
-    code span.fu { color: #06287e; } /* Function */
-    code span.im { color: #008000; font-weight: bold; } /* Import */
-    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
-    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
-    code span.op { color: #666666; } /* Operator */
-    code span.ot { color: #007020; } /* Other */
-    code span.pp { color: #bc7a00; } /* Preprocessor */
-    code span.sc { color: #4070a0; } /* SpecialChar */
-    code span.ss { color: #bb6688; } /* SpecialString */
-    code span.st { color: #4070a0; } /* String */
-    code span.va { color: #19177c; } /* Variable */
-    code span.vs { color: #4070a0; } /* VerbatimString */
-    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
-  </style>
-</head>
-<body>
 <h1 id="lhs-test">lhs test</h1>
 <p><code>unsplit</code> is an arrow that takes a pair of values and combines them to
 return a single value:</p>
@@ -247,5 +12,3 @@
 <blockquote>
 <p>foo bar</p>
 </blockquote>
-</body>
-</html>
diff --git a/test/lhs-test.html+lhs b/test/lhs-test.html+lhs
--- a/test/lhs-test.html+lhs
+++ b/test/lhs-test.html+lhs
@@ -1,238 +1,3 @@
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
-<head>
-  <meta charset="utf-8" />
-  <meta name="generator" content="pandoc" />
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
-  <title>lhs-test</title>
-  <style>
-    html {
-      color: #1a1a1a;
-      background-color: #fdfdfd;
-    }
-    body {
-      margin: 0 auto;
-      max-width: 36em;
-      padding-left: 50px;
-      padding-right: 50px;
-      padding-top: 50px;
-      padding-bottom: 50px;
-      hyphens: auto;
-      overflow-wrap: break-word;
-      text-rendering: optimizeLegibility;
-      font-kerning: normal;
-    }
-    @media (max-width: 600px) {
-      body {
-        font-size: 0.9em;
-        padding: 12px;
-      }
-      h1 {
-        font-size: 1.8em;
-      }
-    }
-    @media print {
-      html {
-        background-color: white;
-      }
-      body {
-        background-color: transparent;
-        color: black;
-        font-size: 12pt;
-      }
-      p, h2, h3 {
-        orphans: 3;
-        widows: 3;
-      }
-      h2, h3, h4 {
-        page-break-after: avoid;
-      }
-    }
-    p {
-      margin: 1em 0;
-    }
-    a {
-      color: #1a1a1a;
-    }
-    a:visited {
-      color: #1a1a1a;
-    }
-    img {
-      max-width: 100%;
-    }
-    svg {
-      height: auto;
-      max-width: 100%;
-    }
-    h1, h2, h3, h4, h5, h6 {
-      margin-top: 1.4em;
-    }
-    h5, h6 {
-      font-size: 1em;
-      font-style: italic;
-    }
-    h6 {
-      font-weight: normal;
-    }
-    ol, ul {
-      padding-left: 1.7em;
-      margin-top: 1em;
-    }
-    li > ol, li > ul {
-      margin-top: 0;
-    }
-    blockquote {
-      margin: 1em 0 1em 1.7em;
-      padding-left: 1em;
-      border-left: 2px solid #e6e6e6;
-      color: #606060;
-    }
-    code {
-      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
-      font-size: 85%;
-      margin: 0;
-      hyphens: manual;
-    }
-    pre {
-      margin: 1em 0;
-      overflow: auto;
-    }
-    pre code {
-      padding: 0;
-      overflow: visible;
-      overflow-wrap: normal;
-    }
-    .sourceCode {
-     background-color: transparent;
-     overflow: visible;
-    }
-    hr {
-      background-color: #1a1a1a;
-      border: none;
-      height: 1px;
-      margin: 1em 0;
-    }
-    table {
-      margin: 1em 0;
-      border-collapse: collapse;
-      width: 100%;
-      overflow-x: auto;
-      display: block;
-      font-variant-numeric: lining-nums tabular-nums;
-    }
-    table caption {
-      margin-bottom: 0.75em;
-    }
-    tbody {
-      margin-top: 0.5em;
-      border-top: 1px solid #1a1a1a;
-      border-bottom: 1px solid #1a1a1a;
-    }
-    th {
-      border-top: 1px solid #1a1a1a;
-      padding: 0.25em 0.5em 0.25em 0.5em;
-    }
-    td {
-      padding: 0.125em 0.5em 0.25em 0.5em;
-    }
-    header {
-      margin-bottom: 4em;
-      text-align: center;
-    }
-    #TOC li {
-      list-style: none;
-    }
-    #TOC ul {
-      padding-left: 1.3em;
-    }
-    #TOC > ul {
-      padding-left: 0;
-    }
-    #TOC a:not(:hover) {
-      text-decoration: none;
-    }
-    code{white-space: pre-wrap;}
-    span.smallcaps{font-variant: small-caps;}
-    div.columns{display: flex; gap: min(4vw, 1.5em);}
-    div.column{flex: auto; overflow-x: auto;}
-    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
-    /* The extra [class] is a hack that increases specificity enough to
-       override a similar rule in reveal.js */
-    ul.task-list[class]{list-style: none;}
-    ul.task-list li input[type="checkbox"] {
-      font-size: inherit;
-      width: 0.8em;
-      margin: 0 0.8em 0.2em -1.6em;
-      vertical-align: middle;
-    }
-    .display.math{display: block; text-align: center; margin: 0.5rem auto;}
-    /* CSS for syntax highlighting */
-    pre > code.sourceCode { white-space: pre; position: relative; }
-    pre > code.sourceCode > span { line-height: 1.25; }
-    pre > code.sourceCode > span:empty { height: 1.2em; }
-    .sourceCode { overflow: visible; }
-    code.sourceCode > span { color: inherit; text-decoration: inherit; }
-    div.sourceCode { margin: 1em 0; }
-    pre.sourceCode { margin: 0; }
-    @media screen {
-    div.sourceCode { overflow: auto; }
-    }
-    @media print {
-    pre > code.sourceCode { white-space: pre-wrap; }
-    pre > code.sourceCode > span { display: inline-block; text-indent: -5em; padding-left: 5em; }
-    }
-    pre.numberSource code
-      { counter-reset: source-line 0; }
-    pre.numberSource code > span
-      { position: relative; left: -4em; counter-increment: source-line; }
-    pre.numberSource code > span > a:first-child::before
-      { content: counter(source-line);
-        position: relative; left: -1em; text-align: right; vertical-align: baseline;
-        border: none; display: inline-block;
-        -webkit-touch-callout: none; -webkit-user-select: none;
-        -khtml-user-select: none; -moz-user-select: none;
-        -ms-user-select: none; user-select: none;
-        padding: 0 4px; width: 4em;
-        color: #aaaaaa;
-      }
-    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
-    div.sourceCode
-      {   }
-    @media screen {
-    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
-    }
-    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
-    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
-    code span.at { color: #7d9029; } /* Attribute */
-    code span.bn { color: #40a070; } /* BaseN */
-    code span.bu { color: #008000; } /* BuiltIn */
-    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
-    code span.ch { color: #4070a0; } /* Char */
-    code span.cn { color: #880000; } /* Constant */
-    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
-    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
-    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
-    code span.dt { color: #902000; } /* DataType */
-    code span.dv { color: #40a070; } /* DecVal */
-    code span.er { color: #ff0000; font-weight: bold; } /* Error */
-    code span.ex { } /* Extension */
-    code span.fl { color: #40a070; } /* Float */
-    code span.fu { color: #06287e; } /* Function */
-    code span.im { color: #008000; font-weight: bold; } /* Import */
-    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
-    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
-    code span.op { color: #666666; } /* Operator */
-    code span.ot { color: #007020; } /* Other */
-    code span.pp { color: #bc7a00; } /* Preprocessor */
-    code span.sc { color: #4070a0; } /* SpecialChar */
-    code span.ss { color: #bb6688; } /* SpecialString */
-    code span.st { color: #4070a0; } /* String */
-    code span.va { color: #19177c; } /* Variable */
-    code span.vs { color: #4070a0; } /* VerbatimString */
-    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
-  </style>
-</head>
-<body>
 <h1 id="lhs-test">lhs test</h1>
 <p><code>unsplit</code> is an arrow that takes a pair of values and combines them to
 return a single value:</p>
@@ -247,5 +12,3 @@
 <blockquote>
 <p>foo bar</p>
 </blockquote>
-</body>
-</html>
diff --git a/test/lhs-test.latex b/test/lhs-test.latex
--- a/test/lhs-test.latex
+++ b/test/lhs-test.latex
@@ -1,98 +1,3 @@
-% Options for packages loaded elsewhere
-\PassOptionsToPackage{unicode}{hyperref}
-\PassOptionsToPackage{hyphens}{url}
-%
-\documentclass[
-]{article}
-\usepackage{amsmath,amssymb}
-\usepackage{iftex}
-\ifPDFTeX
-  \usepackage[T1]{fontenc}
-  \usepackage[utf8]{inputenc}
-  \usepackage{textcomp} % provide euro and other symbols
-\else % if luatex or xetex
-  \usepackage{unicode-math} % this also loads fontspec
-  \defaultfontfeatures{Scale=MatchLowercase}
-  \defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
-\fi
-\usepackage{lmodern}
-\ifPDFTeX\else
-  % xetex/luatex font selection
-\fi
-% Use upquote if available, for straight quotes in verbatim environments
-\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
-\IfFileExists{microtype.sty}{% use microtype if available
-  \usepackage[]{microtype}
-  \UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
-}{}
-\makeatletter
-\@ifundefined{KOMAClassName}{% if non-KOMA class
-  \IfFileExists{parskip.sty}{%
-    \usepackage{parskip}
-  }{% else
-    \setlength{\parindent}{0pt}
-    \setlength{\parskip}{6pt plus 2pt minus 1pt}}
-}{% if KOMA class
-  \KOMAoptions{parskip=half}}
-\makeatother
-\usepackage{xcolor}
-\usepackage{color}
-\usepackage{fancyvrb}
-\newcommand{\VerbBar}{|}
-\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
-\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
-% Add ',fontsize=\small' for more characters per line
-\newenvironment{Shaded}{}{}
-\newcommand{\AlertTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
-\newcommand{\AnnotationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
-\newcommand{\AttributeTok}[1]{\textcolor[rgb]{0.49,0.56,0.16}{#1}}
-\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
-\newcommand{\BuiltInTok}[1]{\textcolor[rgb]{0.00,0.50,0.00}{#1}}
-\newcommand{\CharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
-\newcommand{\CommentTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textit{#1}}}
-\newcommand{\CommentVarTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
-\newcommand{\ConstantTok}[1]{\textcolor[rgb]{0.53,0.00,0.00}{#1}}
-\newcommand{\ControlFlowTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
-\newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.56,0.13,0.00}{#1}}
-\newcommand{\DecValTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
-\newcommand{\DocumentationTok}[1]{\textcolor[rgb]{0.73,0.13,0.13}{\textit{#1}}}
-\newcommand{\ErrorTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
-\newcommand{\ExtensionTok}[1]{#1}
-\newcommand{\FloatTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
-\newcommand{\FunctionTok}[1]{\textcolor[rgb]{0.02,0.16,0.49}{#1}}
-\newcommand{\ImportTok}[1]{\textcolor[rgb]{0.00,0.50,0.00}{\textbf{#1}}}
-\newcommand{\InformationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
-\newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
-\newcommand{\NormalTok}[1]{#1}
-\newcommand{\OperatorTok}[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
-\newcommand{\OtherTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{#1}}
-\newcommand{\PreprocessorTok}[1]{\textcolor[rgb]{0.74,0.48,0.00}{#1}}
-\newcommand{\RegionMarkerTok}[1]{#1}
-\newcommand{\SpecialCharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
-\newcommand{\SpecialStringTok}[1]{\textcolor[rgb]{0.73,0.40,0.53}{#1}}
-\newcommand{\StringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
-\newcommand{\VariableTok}[1]{\textcolor[rgb]{0.10,0.09,0.49}{#1}}
-\newcommand{\VerbatimStringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
-\newcommand{\WarningTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
-\setlength{\emergencystretch}{3em} % prevent overfull lines
-\providecommand{\tightlist}{%
-  \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
-\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
-\ifLuaTeX
-  \usepackage{selnolig}  % disable illegal ligatures
-\fi
-\usepackage{bookmark}
-\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
-\urlstyle{same}
-\hypersetup{
-  hidelinks,
-  pdfcreator={LaTeX via pandoc}}
-
-\author{}
-\date{}
-
-\begin{document}
-
 \section{lhs test}\label{lhs-test}
 
 \texttt{unsplit} is an arrow that takes a pair of values and combines them to
@@ -119,5 +24,3 @@
 \begin{quote}
 foo bar
 \end{quote}
-
-\end{document}
diff --git a/test/lhs-test.latex+lhs b/test/lhs-test.latex+lhs
--- a/test/lhs-test.latex+lhs
+++ b/test/lhs-test.latex+lhs
@@ -1,65 +1,3 @@
-% Options for packages loaded elsewhere
-\PassOptionsToPackage{unicode}{hyperref}
-\PassOptionsToPackage{hyphens}{url}
-%
-\documentclass[
-]{article}
-\usepackage{amsmath,amssymb}
-\usepackage{iftex}
-\ifPDFTeX
-  \usepackage[T1]{fontenc}
-  \usepackage[utf8]{inputenc}
-  \usepackage{textcomp} % provide euro and other symbols
-\else % if luatex or xetex
-  \usepackage{unicode-math} % this also loads fontspec
-  \defaultfontfeatures{Scale=MatchLowercase}
-  \defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
-\fi
-\usepackage{lmodern}
-\ifPDFTeX\else
-  % xetex/luatex font selection
-\fi
-% Use upquote if available, for straight quotes in verbatim environments
-\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
-\IfFileExists{microtype.sty}{% use microtype if available
-  \usepackage[]{microtype}
-  \UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
-}{}
-\makeatletter
-\@ifundefined{KOMAClassName}{% if non-KOMA class
-  \IfFileExists{parskip.sty}{%
-    \usepackage{parskip}
-  }{% else
-    \setlength{\parindent}{0pt}
-    \setlength{\parskip}{6pt plus 2pt minus 1pt}}
-}{% if KOMA class
-  \KOMAoptions{parskip=half}}
-\makeatother
-\usepackage{xcolor}
-\usepackage{listings}
-\newcommand{\passthrough}[1]{#1}
-\lstset{defaultdialect=[5.3]Lua}
-\lstset{defaultdialect=[x86masm]Assembler}
-\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
-\setlength{\emergencystretch}{3em} % prevent overfull lines
-\providecommand{\tightlist}{%
-  \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
-\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
-\ifLuaTeX
-  \usepackage{selnolig}  % disable illegal ligatures
-\fi
-\usepackage{bookmark}
-\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
-\urlstyle{same}
-\hypersetup{
-  hidelinks,
-  pdfcreator={LaTeX via pandoc}}
-
-\author{}
-\date{}
-
-\begin{document}
-
 \section{lhs test}\label{lhs-test}
 
 \texttt{unsplit} is an arrow that takes a pair of values and combines them to
@@ -84,5 +22,3 @@
 \begin{quote}
 foo bar
 \end{quote}
-
-\end{document}
diff --git a/test/tables.typst b/test/tables.typst
--- a/test/tables.typst
+++ b/test/tables.typst
@@ -3,20 +3,12 @@
 #figure(
   align(center)[#table(
     columns: 4,
-    align: (col, row) => (right,left,center,auto,).at(col),
-    [Right], [Left], [Center], [Default],
-    [12],
-    [12],
-    [12],
-    [12],
-    [123],
-    [123],
-    [123],
-    [123],
-    [1],
-    [1],
-    [1],
-    [1],
+    align: (right,left,center,auto,),
+    table.header([Right], [Left], [Center], [Default],),
+    table.hline(),
+    [12], [12], [12], [12],
+    [123], [123], [123], [123],
+    [1], [1], [1], [1],
   )]
   , caption: [Demonstration of simple table syntax.]
   , kind: table
@@ -27,20 +19,12 @@
 #figure(
   align(center)[#table(
     columns: 4,
-    align: (col, row) => (right,left,center,auto,).at(col),
-    [Right], [Left], [Center], [Default],
-    [12],
-    [12],
-    [12],
-    [12],
-    [123],
-    [123],
-    [123],
-    [123],
-    [1],
-    [1],
-    [1],
-    [1],
+    align: (right,left,center,auto,),
+    table.header([Right], [Left], [Center], [Default],),
+    table.hline(),
+    [12], [12], [12], [12],
+    [123], [123], [123], [123],
+    [1], [1], [1], [1],
   )]
   , kind: table
   )
@@ -50,20 +34,12 @@
 #figure(
   align(center)[#table(
     columns: 4,
-    align: (col, row) => (right,left,center,auto,).at(col),
-    [Right], [Left], [Center], [Default],
-    [12],
-    [12],
-    [12],
-    [12],
-    [123],
-    [123],
-    [123],
-    [123],
-    [1],
-    [1],
-    [1],
-    [1],
+    align: (right,left,center,auto,),
+    table.header([Right], [Left], [Center], [Default],),
+    table.hline(),
+    [12], [12], [12], [12],
+    [123], [123], [123], [123],
+    [1], [1], [1], [1],
   )]
   , caption: [Demonstration of simple table syntax.]
   , kind: table
@@ -73,17 +49,14 @@
 
 #figure(
   align(center)[#table(
-    columns: 4,
-    align: (col, row) => (center,left,right,left,).at(col),
-    [Centered Header], [Left Aligned], [Right Aligned], [Default aligned],
-    [First],
-    [row],
-    [12.0],
-    [Example of a row that spans multiple lines.],
-    [Second],
-    [row],
-    [5.0],
-    [Here’s another one. Note the blank line between rows.],
+    columns: (15%, 13.75%, 16.25%, 35%),
+    align: (center,left,right,left,),
+    table.header([Centered Header], [Left Aligned], [Right Aligned], [Default
+      aligned],),
+    table.hline(),
+    [First], [row], [12.0], [Example of a row that spans multiple lines.],
+    [Second], [row], [5.0], [Here’s another one. Note the blank line between
+    rows.],
   )]
   , caption: [Here’s the caption. It may span multiple lines.]
   , kind: table
@@ -93,17 +66,14 @@
 
 #figure(
   align(center)[#table(
-    columns: 4,
-    align: (col, row) => (center,left,right,left,).at(col),
-    [Centered Header], [Left Aligned], [Right Aligned], [Default aligned],
-    [First],
-    [row],
-    [12.0],
-    [Example of a row that spans multiple lines.],
-    [Second],
-    [row],
-    [5.0],
-    [Here’s another one. Note the blank line between rows.],
+    columns: (15%, 13.75%, 16.25%, 35%),
+    align: (center,left,right,left,),
+    table.header([Centered Header], [Left Aligned], [Right Aligned], [Default
+      aligned],),
+    table.hline(),
+    [First], [row], [12.0], [Example of a row that spans multiple lines.],
+    [Second], [row], [5.0], [Here’s another one. Note the blank line between
+    rows.],
   )]
   , kind: table
   )
@@ -113,19 +83,10 @@
 #figure(
   align(center)[#table(
     columns: 4,
-    align: (col, row) => (right,left,center,right,).at(col),
-    [12],
-    [12],
-    [12],
-    [12],
-    [123],
-    [123],
-    [123],
-    [123],
-    [1],
-    [1],
-    [1],
-    [1],
+    align: (right,left,center,right,),
+    [12], [12], [12], [12],
+    [123], [123], [123], [123],
+    [1], [1], [1], [1],
   )]
   , kind: table
   )
@@ -134,16 +95,11 @@
 
 #figure(
   align(center)[#table(
-    columns: 4,
-    align: (col, row) => (center,left,right,auto,).at(col),
-    [First],
-    [row],
-    [12.0],
-    [Example of a row that spans multiple lines.],
-    [Second],
-    [row],
-    [5.0],
-    [Here’s another one. Note the blank line between rows.],
+    columns: (15%, 13.75%, 16.25%, 35%),
+    align: (center,left,right,auto,),
+    [First], [row], [12.0], [Example of a row that spans multiple lines.],
+    [Second], [row], [5.0], [Here’s another one. Note the blank line between
+    rows.],
   )]
   , kind: table
   )
diff --git a/test/tables/nordics.typst b/test/tables/nordics.typst
new file mode 100644
--- /dev/null
+++ b/test/tables/nordics.typst
@@ -0,0 +1,23 @@
+#figure(
+  align(center)[#table(
+    columns: (30%, 30%, 20%, 20%),
+    align: (center,left,left,left,),
+    table.header(table.cell(align: center)[Name], table.cell(align: center)[Capital], table.cell(align: center)[Population
+      \
+      (in 2018)], table.cell(align: center)[Area \
+      (in km#super[2];)],),
+    table.hline(),
+    [Denmark], [Copenhagen], [5,809,502], [43,094],
+    [Finland], [Helsinki], [5,537,364], [338,145],
+    [Iceland], [Reykjavik], [343,518], [103,000],
+    [Norway], [Oslo], [5,372,191], [323,802],
+    [Sweden], [Stockholm], [10,313,447], [450,295],
+    table.hline(),
+    table.footer([Total], [], [27,376,022], [1,258,336],),
+  )]
+  , caption: [States belonging to the #emph[Nordics.]
+
+  ]
+  , kind: table
+  )
+<nordics>
diff --git a/test/tables/planets.typst b/test/tables/planets.typst
new file mode 100644
--- /dev/null
+++ b/test/tables/planets.typst
@@ -0,0 +1,33 @@
+#figure(
+  align(center)[#table(
+    columns: 12,
+    align: (center,center,auto,right,right,right,right,right,right,right,right,auto,),
+    table.header(table.cell(colspan: 2)[], [Name], [Mass (10^24kg)], [Diameter
+      (km)], [Density (kg/m^3)], [Gravity (m/s^2)], [Length of day
+      (hours)], [Distance from Sun (10^6km)], [Mean temperature (C)], [Number of
+      moons], [Notes],),
+    table.hline(),
+    table.cell(rowspan: 4, colspan: 2)[Terrestrial
+    planets], [Mercury], [0.330], [4,879], [5427], [3.7], [4222.6], [57.9], [167], [0], [Closest
+    to the Sun],
+    [Venus], [4.87], [12,104], [5243], [8.9], [2802.0], [108.2], [464], [0], [],
+    [Earth], [5.97], [12,756], [5514], [9.8], [24.0], [149.6], [15], [1], [Our
+    world],
+    [Mars], [0.642], [6,792], [3933], [3.7], [24.7], [227.9], [-65], [2], [The
+    red planet],
+    table.cell(rowspan: 4)[Jovian planets], table.cell(rowspan: 2)[Gas
+    giants], [Jupiter], [1898], [142,984], [1326], [23.1], [9.9], [778.6], [-110], [67], [The
+    largest planet],
+    [Saturn], [568], [120,536], [687], [9.0], [10.7], [1433.5], [-140], [62], [],
+    table.cell(rowspan: 2)[Ice
+    giants], [Uranus], [86.8], [51,118], [1271], [8.7], [17.2], [2872.5], [-195], [27], [],
+    [Neptune], [102], [49,528], [1638], [11.0], [16.1], [4495.1], [-200], [14], [],
+    table.cell(colspan: 2)[Dwarf
+    planets], [Pluto], [0.0146], [2,370], [2095], [0.7], [153.3], [5906.4], [-225], [5], [Declassified
+    as a planet in 2006.],
+  )]
+  , caption: [Data about the planets of our solar system.
+
+  ]
+  , kind: table
+  )
diff --git a/test/tables/students.typst b/test/tables/students.typst
new file mode 100644
--- /dev/null
+++ b/test/tables/students.typst
@@ -0,0 +1,26 @@
+#figure(
+  align(center)[#table(
+    columns: (50%, 50%),
+    align: (left,left,),
+    table.header(table.cell(align: center)[Student
+      ID], table.cell(align: center)[Name],),
+    table.hline(),
+    table.cell(colspan: 2)[Computer Science],
+    table.hline(),
+    [3741255], [Jones, Martha],
+    [4077830], [Pierce, Benjamin],
+    [5151701], [Kirk, James],
+    table.cell(colspan: 2)[Russian Literature],
+    table.hline(),
+    [3971244], [Nim, Victor],
+    table.cell(colspan: 2)[Astrophysics],
+    table.hline(),
+    [4100332], [Petrov, Alexandra],
+    [4100332], [Toyota, Hiroko],
+  )]
+  , caption: [List of Students
+
+  ]
+  , kind: table
+  )
+<students>
diff --git a/test/writer.typst b/test/writer.typst
--- a/test/writer.typst
+++ b/test/writer.typst
@@ -16,7 +16,8 @@
 }
 
 #set table(
-  inset: 6pt
+  inset: 6pt,
+  stroke: none
 )
 
 #let conf(
