LaTeX reader: support \footnotemark and \footnotetext (#11450).

These commands allow separating the footnote mark from its content,
useful in tables, minipages, and other contexts where \footnote
cannot be used directly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Closes #11450.
This commit is contained in:
John MacFarlane
2026-02-14 14:13:47 +00:00
parent 3b9a47d02e
commit 3c56088b4e
3 changed files with 112 additions and 0 deletions
+48
View File
@@ -110,6 +110,7 @@ parseLaTeX = do
(if bottomLevel < 1
then walk (adjustHeaders (1 - bottomLevel))
else id) $
walk (resolveFootnoteMarks (sFootnoteTexts st)) $
walk (resolveRefs (sLabels st)) doc'
return $ Pandoc meta bs'
@@ -124,6 +125,18 @@ resolveRefs labels x@(Link (ident,classes,kvs) _ _) =
_ -> x
resolveRefs _ x = x
-- | Resolve footnote marks (\footnotemark) to actual notes using
-- the footnote texts collected from \footnotetext commands.
resolveFootnoteMarks :: M.Map Int Blocks -> Inline -> Inline
resolveFootnoteMarks fnTexts (Span (_, classes, kvs) _)
| "footnote-mark" `elem` classes
, Just numText <- lookup "note-num" kvs
, [(n, "")] <- reads (T.unpack numText)
= case M.lookup n fnTexts of
Just contents -> Note (toList contents)
Nothing -> Str "" -- No matching footnotetext found
resolveFootnoteMarks _ x = x
-- testParser :: LP PandocIO a -> Text -> IO a
-- testParser p t = do
@@ -407,6 +420,8 @@ inlineCommands = M.unions
, ("lowercase", makeLowercase <$> tok)
, ("thanks", skipopts >> note <$> grouped block)
, ("footnote", skipopts >> footnote)
, ("footnotemark", footnotemark)
, ("footnotetext", footnotetext)
, ("newline", pure B.linebreak)
, ("passthrough", fixPassthroughEscapes <$> tok)
-- \passthrough macro used by latex writer
@@ -477,6 +492,39 @@ footnote = do
contents <- grouped block >>= walkM resolveNoteLabel
return $ note contents
-- | Parse \footnotemark[n]. If n is not given, increment the counter.
-- Returns a span marker that will be resolved to a Note later.
footnotemark :: PandocMonad m => LP m Inlines
footnotemark = do
mbNum <- optionalFootnoteNum
noteNum <- case mbNum of
Just n -> return n
Nothing -> do
updateState $ \st -> st{ sLastNoteNum = sLastNoteNum st + 1 }
sLastNoteNum <$> getState
return $ B.spanWith ("", ["footnote-mark"], [("note-num", tshow noteNum)]) mempty
-- | Parse \footnotetext[n]{text}. If n is not given, use current counter.
-- Stores the text in state to be resolved later.
footnotetext :: PandocMonad m => LP m Inlines
footnotetext = do
mbNum <- optionalFootnoteNum
noteNum <- case mbNum of
Just n -> return n
Nothing -> sLastNoteNum <$> getState
contents <- grouped block >>= walkM resolveNoteLabel
updateState $ \st -> st{
sFootnoteTexts = M.insert noteNum contents (sFootnoteTexts st) }
return mempty
-- | Parse optional footnote number argument [n]
optionalFootnoteNum :: PandocMonad m => LP m (Maybe Int)
optionalFootnoteNum = option Nothing $ do
t <- bracketedToks
case reads (T.unpack $ untokenize t) of
[(n, "")] -> return $ Just n
_ -> return Nothing
resolveNoteLabel :: PandocMonad m => Inline -> LP m Inline
resolveNoteLabel (Span (_,cls,kvs) _)
| Just lab <- lookup "label" kvs = do
+2
View File
@@ -165,6 +165,7 @@ data LaTeXState = LaTeXState{ sOptions :: ReaderOptions
, sLastFigureNum :: DottedNum
, sLastTableNum :: DottedNum
, sLastNoteNum :: Int
, sFootnoteTexts :: M.Map Int Blocks
, sTheoremMap :: M.Map Text TheoremSpec
, sLastTheoremStyle :: TheoremStyle
, sLastLabel :: Maybe Text
@@ -195,6 +196,7 @@ defaultLaTeXState = LaTeXState{ sOptions = def
, sLastFigureNum = DottedNum []
, sLastTableNum = DottedNum []
, sLastNoteNum = 0
, sFootnoteTexts = M.empty
, sTheoremMap = M.empty
, sLastTheoremStyle = PlainStyle
, sLastLabel = Nothing
+62
View File
@@ -0,0 +1,62 @@
Test for \footnotemark and \footnotetext (issue #11450)
```
% pandoc -f latex -t native
Text\footnotemark{}.
\footnotetext{The footnote content.}
^D
[ Para
[ Str "Text"
, Note
[ Para
[ Str "The"
, Space
, Str "footnote"
, Space
, Str "content."
]
]
, Str "."
]
]
```
With explicit footnote numbers:
```
% pandoc -f latex -t native
First\footnotemark[1] and second\footnotemark[2].
\footnotetext[1]{First note.}
\footnotetext[2]{Second note.}
^D
[ Para
[ Str "First"
, Note [ Para [ Str "First" , Space , Str "note." ] ]
, Space
, Str "and"
, Space
, Str "second"
, Note [ Para [ Str "Second" , Space , Str "note." ] ]
, Str "."
]
]
```
Mixed with regular footnotes:
```
% pandoc -f latex -t native
Text\footnotemark[1] and more\footnote{Regular footnote.}
\footnotetext[1]{Marked footnote.}
^D
[ Para
[ Str "Text"
, Note [ Para [ Str "Marked" , Space , Str "footnote." ] ]
, Space
, Str "and"
, Space
, Str "more"
, Note [ Para [ Str "Regular" , Space , Str "footnote." ] ]
]
]
```