From 1ffeb856eebe0bb7e0550fa89bbe15c175f63c41 Mon Sep 17 00:00:00 2001
From: Meher Chaitanya <53662575+smc181002@users.noreply.github.com>
Date: Sat, 30 May 2026 22:41:14 +0530
Subject: [PATCH] Add auto_identifiers support to Man Reader (#11675)
Add support for the `auto_identifiers`, `gfm_auto_identifiers`, and
`ascii_identifiers` extensions in the man reader. Section headings
parsed from .SH and .SS macros now receive auto-generated id
attributes when the extension is enabled, enabling `--toc` to
produce working anchor links.
- Add `autoIdExtensions` to default man extensions [behavior change]
- Add `HasReaderOptions`, `HasLogMessages` and `HasIdentifierList` to
`ManState` to run `registerHeader`
Closes #8852.
---
MANUAL.txt | 4 ++--
src/Text/Pandoc/Extensions.hs | 1 +
src/Text/Pandoc/Readers/Man.hs | 22 ++++++++++++++++-
test/command/11635.md | 2 +-
test/command/11668.md | 2 +-
test/command/8852.md | 43 ++++++++++++++++++++++++++++++++++
test/man-reader.native | 36 +++++++++++++++-------------
7 files changed, 89 insertions(+), 21 deletions(-)
create mode 100644 test/command/8852.md
diff --git a/MANUAL.txt b/MANUAL.txt
index e2a3e90d7..64884e69a 100644
--- a/MANUAL.txt
+++ b/MANUAL.txt
@@ -3668,13 +3668,13 @@ automatically assigned a unique identifier based on the heading text.
This extension can be enabled/disabled for the following formats:
input formats
-: `markdown`, `latex`, `rst`, `mediawiki`, `textile`
+: `markdown`, `latex`, `rst`, `mediawiki`, `textile`, `man`
output formats
: `markdown`, `muse`
enabled by default in
-: `markdown`, `muse`
+: `markdown`, `muse`, `man`
The default algorithm used to derive the identifier from the
heading text is:
diff --git a/src/Text/Pandoc/Extensions.hs b/src/Text/Pandoc/Extensions.hs
index 7fcb214cd..71b5da799 100644
--- a/src/Text/Pandoc/Extensions.hs
+++ b/src/Text/Pandoc/Extensions.hs
@@ -664,4 +664,5 @@ getAllExtensions f = universalExtensions <> getAll f
[ Ext_smart ]
getAll "typst" = extensionsFromList [Ext_citations, Ext_smart]
getAll "djot" = extensionsFromList [Ext_sourcepos]
+ getAll "man" = autoIdExtensions
getAll _ = mempty
diff --git a/src/Text/Pandoc/Readers/Man.hs b/src/Text/Pandoc/Readers/Man.hs
index 50586daf8..0b5d58d69 100644
--- a/src/Text/Pandoc/Readers/Man.hs
+++ b/src/Text/Pandoc/Readers/Man.hs
@@ -26,23 +26,40 @@ import Text.Pandoc.Builder as B
import Text.Pandoc.Class.PandocMonad (PandocMonad(..), report)
import Text.Pandoc.Logging (LogMessage(..))
import Text.Pandoc.Options
+import Text.Pandoc.Parsing.Capabilities
import Text.Pandoc.Parsing
import Text.Pandoc.Walk (query)
import Text.Pandoc.Readers.Roff -- TODO explicit imports
import qualified Text.Pandoc.Parsing as P
import qualified Data.Foldable as Foldable
+import qualified Data.Set as Set
import Text.Pandoc.Shared (extractSpaces)
data ManState = ManState { readerOptions :: ReaderOptions
+ , manLogMessages :: []LogMessage
+ , manIdentifiers :: Set.Set T.Text
, metadata :: Meta
, tableCellsPlain :: Bool
} deriving Show
instance Default ManState where
def = ManState { readerOptions = def
+ , manLogMessages = []
+ , manIdentifiers = Set.empty
, metadata = nullMeta
, tableCellsPlain = True }
+instance HasReaderOptions ManState where
+ extractReaderOptions = readerOptions
+
+instance HasLogMessages ManState where
+ addLogMessage msg st = st{ manLogMessages = msg : manLogMessages st }
+ getLogMessages st = reverse $ manLogMessages st
+
+instance HasIdentifierList ManState where
+ extractIdentifierList = manIdentifiers
+ updateIdentifierList f st = st{ manIdentifiers = f $ manIdentifiers st }
+
type ManParser m = P.ParsecT [RoffToken] ManState m
@@ -74,6 +91,7 @@ parseMan = do
bs <- many parseBlock <* eof
meta <- metadata <$> getState
let (Pandoc _ blocks) = doc $ mconcat bs
+ reportLogMessages
return $ Pandoc meta blocks
parseBlock :: PandocMonad m => ManParser m Blocks
@@ -417,7 +435,9 @@ parseHeader = do
else return $ mconcat $ intersperse B.space
$ map linePartsToInlines args
let lvl = if name == "SH" then 1 else 2
- return $ header lvl contents
+ attr <- registerHeader nullAttr contents
+
+ return $ B.headerWith attr lvl contents
parseBlockQuote :: PandocMonad m => ManParser m Blocks
parseBlockQuote = blockQuote <$>
diff --git a/test/command/11635.md b/test/command/11635.md
index 1639c82f6..cf0674935 100644
--- a/test/command/11635.md
+++ b/test/command/11635.md
@@ -11,7 +11,7 @@ another \
one;123456
.TE
^D
-
HEADING
+HEADING
diff --git a/test/command/11668.md b/test/command/11668.md
index 664d992f3..6fbb1d134 100644
--- a/test/command/11668.md
+++ b/test/command/11668.md
@@ -16,7 +16,7 @@ This should still be indented, until some other macro is called to change it.
Like this.
^D
TH "TEST" "1" "2026-05-25" "test v1.0.0" "test manual"
-HEADING
+HEADING
- This is the unindented tag.
-
diff --git a/test/command/8852.md b/test/command/8852.md
new file mode 100644
index 000000000..e9e9b7522
--- /dev/null
+++ b/test/command/8852.md
@@ -0,0 +1,43 @@
+Github Formatted Markdown Identifiers
+```
+% pandoc -f man+gfm_auto_identifiers -t html
+.TH "TEST" "1" "2026-05-08" "test v1.0.0" "test manual"
+.SH 1st HEADING
+.SH C++ & Rust
+.SH HEADING
+.SH HEADING
+^D
+
1st HEADING
+C++ & Rust
+HEADING
+HEADING
+```
+
+Ascii Identifiers test
+```
+% pandoc -f man+ascii_identifiers -t html
+.TH "TEST" "1" "2026-05-08" "test v1.0.0" "test manual"
+.SH Über den Flüssen
+^D
+Über den Flüssen
+```
+
+Headers without Auto Identifiers test
+```
+% pandoc -f man-auto_identifiers -t native
+.TH "TEST" "1" "2026-05-08" "test v1.0.0" "test manual"
+.SS Level 2
+.SS different styles:
+.SS Ordered
+.SS Ordered
+^D
+[ Header
+ 2 ( "" , [] , [] ) [ Str "Level" , Space , Str "2" ]
+, Header
+ 2
+ ( "" , [] , [] )
+ [ Str "different" , Space , Str "styles:" ]
+, Header 2 ( "" , [] , [] ) [ Str "Ordered" ]
+, Header 2 ( "" , [] , [] ) [ Str "Ordered" ]
+]
+```
diff --git a/test/man-reader.native b/test/man-reader.native
index e0d14ca79..117793fcd 100644
--- a/test/man-reader.native
+++ b/test/man-reader.native
@@ -42,11 +42,11 @@ Pandoc
, Space
, Str "*"
]
- , Header 1 ( "" , [] , [] ) [ Str "Headers" ]
+ , Header 1 ( "headers" , [] , [] ) [ Str "Headers" ]
, Header
- 1 ( "" , [] , [] ) [ Str "Level" , Space , Str "1" ]
+ 1 ( "level-1" , [] , [] ) [ Str "Level" , Space , Str "1" ]
, Header
- 2 ( "" , [] , [] ) [ Str "Level" , Space , Str "2" ]
+ 2 ( "level-2" , [] , [] ) [ Str "Level" , Space , Str "2" ]
, Para
[ Str "*"
, Space
@@ -58,7 +58,7 @@ Pandoc
, Space
, Str "*"
]
- , Header 1 ( "" , [] , [] ) [ Str "Paragraphs" ]
+ , Header 1 ( "paragraphs" , [] , [] ) [ Str "Paragraphs" ]
, Para
[ Str "Here's"
, Space
@@ -159,7 +159,9 @@ Pandoc
, Str "*"
]
, Header
- 1 ( "" , [] , [] ) [ Str "Block" , Space , Str "Quotes" ]
+ 1
+ ( "block-quotes" , [] , [] )
+ [ Str "Block" , Space , Str "Quotes" ]
, Para
[ Str "Code"
, Space
@@ -182,7 +184,9 @@ Pandoc
, [ Para [ Str "item" , Space , Str "two" ] ]
]
, Header
- 1 ( "" , [] , [] ) [ Str "Code" , Space , Str "Blocks" ]
+ 1
+ ( "code-blocks" , [] , [] )
+ [ Str "Code" , Space , Str "Blocks" ]
, Para [ Str "Code:" ]
, CodeBlock
( "" , [] , [] )
@@ -201,22 +205,22 @@ Pandoc
, Space
, Str "*"
]
- , Header 1 ( "" , [] , [] ) [ Str "Lists" ]
- , Header 2 ( "" , [] , [] ) [ Str "Unordered" ]
+ , Header 1 ( "lists" , [] , [] ) [ Str "Lists" ]
+ , Header 2 ( "unordered" , [] , [] ) [ Str "Unordered" ]
, Para [ Str "Asterisks:" ]
, BulletList
[ [ Para [ Str "asterisk" , Space , Str "1" ] ]
, [ Para [ Str "asterisk" , Space , Str "2" ] ]
, [ Para [ Str "asterisk" , Space , Str "3" ] ]
]
- , Header 2 ( "" , [] , [] ) [ Str "Ordered" ]
+ , Header 2 ( "ordered" , [] , [] ) [ Str "Ordered" ]
, OrderedList
( 1 , Decimal , Period )
[ [ Para [ Str "First" ] ]
, [ Para [ Str "Second" ] ]
, [ Para [ Str "Third" ] ]
]
- , Header 2 ( "" , [] , [] ) [ Str "Nested" ]
+ , Header 2 ( "nested" , [] , [] ) [ Str "Nested" ]
, BulletList
[ [ Para [ Str "Tab" ]
, BulletList
@@ -254,7 +258,7 @@ Pandoc
]
, Header
2
- ( "" , [] , [] )
+ ( "different-styles" , [] , [] )
[ Str "different" , Space , Str "styles:" ]
, OrderedList
( 1 , UpperAlpha , Period )
@@ -292,7 +296,7 @@ Pandoc
]
]
]
- , Header 2 ( "" , [] , [] ) [ Str "Ordered" ]
+ , Header 2 ( "ordered-1" , [] , [] ) [ Str "Ordered" ]
, Para [ Str "Definition" , Space , Str "lists" ]
, DefinitionList
[ ( [ Strong [ Str "term1" ] ]
@@ -323,7 +327,7 @@ Pandoc
]
, Header
1
- ( "" , [] , [] )
+ ( "special-characters" , [] , [] )
[ Str "Special" , Space , Str "Characters" ]
, Para
[ Str "AT&T"
@@ -364,7 +368,7 @@ Pandoc
, Para [ Str "Bang:" , Space , Str "!" ]
, Para [ Str "Plus:" , Space , Str "+" ]
, Para [ Str "Minus:" , Space , Str "-" ]
- , Header 1 ( "" , [] , [] ) [ Str "Links" ]
+ , Header 1 ( "links" , [] , [] ) [ Str "Links" ]
, Para
[ Link
( "" , [] , [] )
@@ -379,7 +383,7 @@ Pandoc
( "mailto:me@example.com" , "" )
, Str "."
]
- , Header 1 ( "" , [] , [] ) [ Str "Macros" ]
+ , Header 1 ( "macros" , [] , [] ) [ Str "Macros" ]
, Para
[ Strong [ Str "Me" , Space , Str "Myself" ]
, Space
@@ -406,7 +410,7 @@ Pandoc
, Strong [ Str "Author" ]
, Str "."
]
- , Header 1 ( "" , [] , [] ) [ Str "Tables" ]
+ , Header 1 ( "tables" , [] , [] ) [ Str "Tables" ]
, Table
( "" , [] , [] )
(Caption Nothing [])