From ee062dcf0e660fc8c58b1db8ce76d20ab9f3ff67 Mon Sep 17 00:00:00 2001 From: massifrg Date: Thu, 5 Mar 2026 19:20:08 +0100 Subject: [PATCH 01/11] Markdown: extension "endnotes" added The extension "endnotes" to support endnotes specified as a Note embedded in a Span with class "endnote" has been added to markdown. The extension is disabled by default. --- src/Text/Pandoc/Extensions.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Text/Pandoc/Extensions.hs b/src/Text/Pandoc/Extensions.hs index 7fcb214cdbf4..eb2a5e27a402 100644 --- a/src/Text/Pandoc/Extensions.hs +++ b/src/Text/Pandoc/Extensions.hs @@ -68,6 +68,7 @@ data Extension = | Ext_element_citations -- ^ Use element-citation elements for JATS citations | Ext_emoji -- ^ Support emoji like :smile: | Ext_empty_paragraphs -- ^ Allow empty paragraphs + | Ext_endnotes -- ^ Endnotes support when footnotes are embedded in a Span.endnote | Ext_epub_html_exts -- ^ Recognise the EPUB extended version of HTML | Ext_escaped_line_breaks -- ^ Treat a backslash at EOL as linebreak | Ext_example_lists -- ^ Markdown-style numbered examples @@ -505,6 +506,7 @@ getAllExtensions f = universalExtensions <> getAll f , Ext_ignore_line_breaks , Ext_east_asian_line_breaks , Ext_emoji + , Ext_endnotes , Ext_tex_math_single_backslash , Ext_tex_math_double_backslash , Ext_markdown_attribute @@ -532,6 +534,7 @@ getAllExtensions f = universalExtensions <> getAll f [ Ext_raw_markdown ] getAll "docx" = autoIdExtensions <> extensionsFromList [ Ext_empty_paragraphs + , Ext_endnotes , Ext_native_numbering , Ext_styles , Ext_citations From 33a7a7f43d820acee1bde9a7089ca44aa1e20507 Mon Sep 17 00:00:00 2001 From: massifrg Date: Thu, 5 Mar 2026 22:39:54 +0100 Subject: [PATCH 02/11] Markdown reader: support for endnotes When the "endnotes" extension is enabled, notes with a reference starting with "EN" are embedded in a Span with class "endnote". "EN" is the value of a constant `endnotePrefix`, which could set with an option in the future. --- src/Text/Pandoc/Readers/Markdown.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Text/Pandoc/Readers/Markdown.hs b/src/Text/Pandoc/Readers/Markdown.hs index 466b88a10d21..4a6b695aa12c 100644 --- a/src/Text/Pandoc/Readers/Markdown.hs +++ b/src/Text/Pandoc/Readers/Markdown.hs @@ -2060,6 +2060,9 @@ image = try $ do _ -> B.imageWith attr' src regLink constructor lab <|> referenceLink constructor (lab, "!" <> raw) +endnotePrefix :: Text +endnotePrefix = "EN" + note :: PandocMonad m => MarkdownParser m (F Inlines) note = try $ do guardEnabled Ext_footnotes @@ -2082,7 +2085,10 @@ note = try $ do let adjustCite (Cite cs ils) = Cite (map addCitationNoteNum cs) ils adjustCite x = x - return $ B.note $ walk adjustCite contents' + let isEndnoteEnabled = isEnabled Ext_endnotes $ stateOptions st + return $ if isEndnoteEnabled && endnotePrefix `T.isPrefixOf` ref + then B.spanWith ("", ["endnote"], []) $ B.note $ walk adjustCite contents' + else B.note $ walk adjustCite contents' inlineNote :: PandocMonad m => MarkdownParser m (F Inlines) inlineNote = do From bb66f1abe56c9d14abf62c5291f7f2f07d71aabe Mon Sep 17 00:00:00 2001 From: massifrg Date: Thu, 5 Mar 2026 22:42:03 +0100 Subject: [PATCH 03/11] Extensions: removed endnotes extension from docx When checking out Extensions.hs from the docx-endnotes branch, I forgot to remove the "endnotes" extension from docx, since this branch comes from "main". --- src/Text/Pandoc/Extensions.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Text/Pandoc/Extensions.hs b/src/Text/Pandoc/Extensions.hs index eb2a5e27a402..07fbb32f14b1 100644 --- a/src/Text/Pandoc/Extensions.hs +++ b/src/Text/Pandoc/Extensions.hs @@ -534,7 +534,6 @@ getAllExtensions f = universalExtensions <> getAll f [ Ext_raw_markdown ] getAll "docx" = autoIdExtensions <> extensionsFromList [ Ext_empty_paragraphs - , Ext_endnotes , Ext_native_numbering , Ext_styles , Ext_citations From a407bca0e0be63093eff37883a6409a3b1de9dcf Mon Sep 17 00:00:00 2001 From: massifrg Date: Thu, 5 Mar 2026 22:49:10 +0100 Subject: [PATCH 04/11] Markdown writer: WriterState, fields for endnotes stEndnotes and stEndnoteNum have been added to WriterState, along with stNotes and stNoteNum. The goal is to put every Note, that is embedded in a Span.endnote, in stEndnotes, so to have two distinct groups of notes at the end, and endnotes' references prefixed with "EN". --- src/Text/Pandoc/Writers/Markdown/Types.hs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Text/Pandoc/Writers/Markdown/Types.hs b/src/Text/Pandoc/Writers/Markdown/Types.hs index 1e2afe43b56f..af84eedeb635 100644 --- a/src/Text/Pandoc/Writers/Markdown/Types.hs +++ b/src/Text/Pandoc/Writers/Markdown/Types.hs @@ -58,14 +58,16 @@ instance Default WriterEnv , envEscapeSpaces = False } -data WriterState = WriterState { stNotes :: Notes - , stPrevRefs :: Refs - , stRefs :: Refs - , stKeys :: M.Map Key - (M.Map (Target, Attr) Int) - , stLastIdx :: Int - , stIds :: Set.Set Text - , stNoteNum :: Int +data WriterState = WriterState { stNotes :: Notes + , stPrevRefs :: Refs + , stRefs :: Refs + , stKeys :: M.Map Key + (M.Map (Target, Attr) Int) + , stLastIdx :: Int + , stIds :: Set.Set Text + , stNoteNum :: Int + , stEndnotes :: Notes + , stEndnoteNum :: Int } instance Default WriterState @@ -76,4 +78,6 @@ instance Default WriterState , stLastIdx = 0 , stIds = Set.empty , stNoteNum = 1 + , stEndnotes = [] + , stEndnoteNum = 1 } From 3ce8a9a88d5c05c7ad6720c2bf10450a168b2502 Mon Sep 17 00:00:00 2001 From: massifrg Date: Fri, 6 Mar 2026 12:46:26 +0100 Subject: [PATCH 05/11] CLI options: new option --endnotes-prefix The new option is used when the "endnotes" extensions is enabled to set the prefix of endnotes in markdown, both in the Reader and in the Writer. If not set, the current default is "EN". --- pandoc-server/src/Text/Pandoc/Server.hs | 2 ++ src/Text/Pandoc/App.hs | 1 + src/Text/Pandoc/App/CommandLineOptions.hs | 12 +++++++++--- src/Text/Pandoc/App/Opt.hs | 7 +++++++ src/Text/Pandoc/App/OutputSettings.hs | 1 + src/Text/Pandoc/Options.hs | 8 ++++++++ 6 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pandoc-server/src/Text/Pandoc/Server.hs b/pandoc-server/src/Text/Pandoc/Server.hs index bfa2efb32431..e5b078907cf6 100644 --- a/pandoc-server/src/Text/Pandoc/Server.hs +++ b/pandoc-server/src/Text/Pandoc/Server.hs @@ -304,6 +304,7 @@ server = convertBytes optDefaultImageExtension opts , readerTrackChanges = optTrackChanges opts , readerStripComments = optStripComments opts + , readerEndnotesPrefix = optEndnotesPrefix opts } let writeropts = WriterOptions @@ -326,6 +327,7 @@ server = convertBytes , writerDpi = optDpi opts , writerEmailObfuscation = optEmailObfuscation opts , writerIdentifierPrefix = optIdentifierPrefix opts + , writerEndnotesPrefix = optEndnotesPrefix opts , writerCiteMethod = optCiteMethod opts , writerHtmlQTags = optHtmlQTags opts , writerSlideLevel = optSlideLevel opts diff --git a/src/Text/Pandoc/App.hs b/src/Text/Pandoc/App.hs index 67cbd6158b8d..e336053cc664 100644 --- a/src/Text/Pandoc/App.hs +++ b/src/Text/Pandoc/App.hs @@ -237,6 +237,7 @@ convertWithOpts' scriptingEngine istty datadir opts = do , readerAbbreviations = abbrevs , readerExtensions = readerExts , readerStripComments = optStripComments opts + , readerEndnotesPrefix = optEndnotesPrefix opts } metadataFromFile <- getMetadataFromFiles readerNameBase readerOpts diff --git a/src/Text/Pandoc/App/CommandLineOptions.hs b/src/Text/Pandoc/App/CommandLineOptions.hs index 1020d716d75f..4872c9245a8f 100644 --- a/src/Text/Pandoc/App/CommandLineOptions.hs +++ b/src/Text/Pandoc/App/CommandLineOptions.hs @@ -882,12 +882,18 @@ options = "none|javascript|references") "" -- "Method for obfuscating email in HTML" - , Option "" ["id-prefix"] - (ReqArg - (\arg opt -> return opt { optIdentifierPrefix = T.pack arg }) + , Option "" ["id-prefix"] + (ReqArg + (\arg opt -> return opt { optIdentifierPrefix = T.pack arg }) "STRING") "" -- "Prefix to add to automatically generated HTML identifiers" + , Option "" ["endnotes-prefix"] + (ReqArg + (\arg opt -> return opt { optEndnotesPrefix = T.pack arg }) + "STRING") + "" -- "Prefix to add to endnotes or to be used to discriminate between footnote and endnote refs (only with endnotes extension)" + , Option "T" ["title-prefix"] (ReqArg (\arg opt -> diff --git a/src/Text/Pandoc/App/Opt.hs b/src/Text/Pandoc/App/Opt.hs index ac93591470e1..ce3335b3e0d1 100644 --- a/src/Text/Pandoc/App/Opt.hs +++ b/src/Text/Pandoc/App/Opt.hs @@ -45,6 +45,7 @@ import Text.Pandoc.Options (TopLevelDivision (TopLevelDefault), CaptionPosition (..), ObfuscationMethod (NoObfuscation), CiteMethod (Citeproc), + defaultEndnotesPrefix, pattern DefaultHighlightingString) import Text.Pandoc.Class (readFileStrict, fileExists, setVerbosity, report, PandocMonad(lookupEnv), getUserDataDir) @@ -156,6 +157,7 @@ data Opt = Opt , optFilters :: [Filter] -- ^ Filters to apply , optEmailObfuscation :: ObfuscationMethod , optIdentifierPrefix :: Text + , optEndnotesPrefix :: Text -- ^ With endnotes extension, the endnotes refs' prefix , optIndentedCodeClasses :: [Text] -- ^ Default classes for indented code blocks , optDataDir :: Maybe FilePath , optCiteMethod :: CiteMethod -- ^ Method to output cites @@ -243,6 +245,7 @@ instance FromJSON Opt where <*> o .:? "filters" .!= optFilters defaultOpts <*> o .:? "email-obfuscation" .!= optEmailObfuscation defaultOpts <*> o .:? "identifier-prefix" .!= optIdentifierPrefix defaultOpts + <*> o .:? "endnotes-prefix" .!= optEndnotesPrefix defaultOpts <*> o .:? "indented-code-classes" .!= optIndentedCodeClasses defaultOpts <*> o .:? "data-dir" <*> o .:? "cite-method" .!= optCiteMethod defaultOpts @@ -653,6 +656,9 @@ doOpt (k,v) = do "identifier-prefix" -> parseJSON v >>= \x -> return (\o -> o{ optIdentifierPrefix = x }) + "endnotes-prefix" -> + parseJSON v >>= \x -> + return (\o -> o{ optEndnotesPrefix = x }) "indented-code-classes" -> parseJSON v >>= \x -> return (\o -> o{ optIndentedCodeClasses = x }) @@ -820,6 +826,7 @@ defaultOpts = Opt , optFilters = [] , optEmailObfuscation = NoObfuscation , optIdentifierPrefix = "" + , optEndnotesPrefix = defaultEndnotesPrefix , optIndentedCodeClasses = [] , optDataDir = Nothing , optCiteMethod = Citeproc diff --git a/src/Text/Pandoc/App/OutputSettings.hs b/src/Text/Pandoc/App/OutputSettings.hs index 65761397882e..4ac7857f91f5 100644 --- a/src/Text/Pandoc/App/OutputSettings.hs +++ b/src/Text/Pandoc/App/OutputSettings.hs @@ -253,6 +253,7 @@ optToOutputSettings scriptingEngine opts = do , writerColumns = optColumns opts , writerEmailObfuscation = optEmailObfuscation opts , writerIdentifierPrefix = optIdentifierPrefix opts + , writerEndnotesPrefix = optEndnotesPrefix opts , writerHtmlQTags = optHtmlQTags opts , writerTopLevelDivision = optTopLevelDivision opts , writerSlideLevel = optSlideLevel opts diff --git a/src/Text/Pandoc/Options.hs b/src/Text/Pandoc/Options.hs index 1079a374812d..29b428660a6c 100644 --- a/src/Text/Pandoc/Options.hs +++ b/src/Text/Pandoc/Options.hs @@ -36,6 +36,7 @@ module Text.Pandoc.Options ( module Text.Pandoc.Extensions , CaptionPosition (..) , def , isEnabled + , defaultEndnotesPrefix , defaultMathJaxURL , defaultWebTeXURL , defaultKaTeXURL @@ -74,11 +75,15 @@ data ReaderOptions = ReaderOptions{ , readerTrackChanges :: TrackChanges -- ^ Track changes setting for docx , readerStripComments :: Bool -- ^ Strip HTML comments instead of parsing as raw HTML -- (only implemented in commonmark) + , readerEndnotesPrefix :: Text -- ^ Endnotes' prefix when endnotes extension is enabled } deriving (Show, Read, Data, Typeable, Generic) instance HasSyntaxExtensions ReaderOptions where getExtensions opts = readerExtensions opts +defaultEndnotesPrefix :: Text +defaultEndnotesPrefix = "EN" + instance Default ReaderOptions where def = ReaderOptions{ readerExtensions = emptyExtensions @@ -90,6 +95,7 @@ instance Default ReaderOptions , readerDefaultImageExtension = "" , readerTrackChanges = AcceptChanges , readerStripComments = False + , readerEndnotesPrefix = defaultEndnotesPrefix } defaultAbbrevs :: Set.Set Text @@ -390,6 +396,7 @@ data WriterOptions = WriterOptions , writerSyntaxMap :: SyntaxMap , writerPreferAscii :: Bool -- ^ Prefer ASCII representations of characters when possible , writerLinkImages :: Bool -- ^ Use links rather than embedding ODT images + , writerEndnotesPrefix :: Text -- ^ Prefix for endnotes refs when endnotes extension is enabled } deriving (Show, Data, Typeable, Generic) instance Default WriterOptions where @@ -432,6 +439,7 @@ instance Default WriterOptions where , writerSyntaxMap = defaultSyntaxMap , writerPreferAscii = False , writerLinkImages = False + , writerEndnotesPrefix = defaultEndnotesPrefix } instance HasSyntaxExtensions WriterOptions where From ffe6a5a4bca9755049c7e85a04fe9a3dfac85f25 Mon Sep 17 00:00:00 2001 From: massifrg Date: Fri, 6 Mar 2026 12:53:33 +0100 Subject: [PATCH 06/11] Markdown reader: --endnotes-prefix When "endnotes" extension is enabled, the markdown Reader uses the value of option --endnotes-prefix to discriminate between footnotes and endnotes (which are the ones whose ref starts with that prefix). --- src/Text/Pandoc/Readers/Markdown.hs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Text/Pandoc/Readers/Markdown.hs b/src/Text/Pandoc/Readers/Markdown.hs index 4a6b695aa12c..797d2c10ddd8 100644 --- a/src/Text/Pandoc/Readers/Markdown.hs +++ b/src/Text/Pandoc/Readers/Markdown.hs @@ -2060,9 +2060,6 @@ image = try $ do _ -> B.imageWith attr' src regLink constructor lab <|> referenceLink constructor (lab, "!" <> raw) -endnotePrefix :: Text -endnotePrefix = "EN" - note :: PandocMonad m => MarkdownParser m (F Inlines) note = try $ do guardEnabled Ext_footnotes @@ -2085,8 +2082,9 @@ note = try $ do let adjustCite (Cite cs ils) = Cite (map addCitationNoteNum cs) ils adjustCite x = x - let isEndnoteEnabled = isEnabled Ext_endnotes $ stateOptions st - return $ if isEndnoteEnabled && endnotePrefix `T.isPrefixOf` ref + let opts = stateOptions st + return $ if isEnabled Ext_endnotes opts + && readerEndnotesPrefix opts `T.isPrefixOf` ref then B.spanWith ("", ["endnote"], []) $ B.note $ walk adjustCite contents' else B.note $ walk adjustCite contents' From abbda97f3a45ff86de24f16160fd61c1d1eb1ab5 Mon Sep 17 00:00:00 2001 From: massifrg Date: Fri, 6 Mar 2026 16:12:40 +0100 Subject: [PATCH 07/11] Markdown reader: endnotes ext tests --- test/command/11503.md | 80 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 test/command/11503.md diff --git a/test/command/11503.md b/test/command/11503.md new file mode 100644 index 000000000000..eccce3c97cee --- /dev/null +++ b/test/command/11503.md @@ -0,0 +1,80 @@ +``` +% pandoc -f markdown+endnotes -t markdown +A paragraph with an endnote[^EN1] and a footnote[^1]. + +Another paragraph with another endnote[^EN2] and another footnote[^2]. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^EN1]: First endnote. + +[^EN2]: Second endnote. +^D +A paragraph with an endnote[[^1]]{.endnote} and a footnote[^2]. + +Another paragraph with another endnote[[^3]]{.endnote} and another +footnote[^4]. + +[^1]: First endnote. + +[^2]: First footnote. + +[^3]: Second endnote. + +[^4]: Second footnote. +``` +``` +% pandoc --endnotes-prefix=e -f markdown+endnotes -t markdown +A paragraph with an endnote[^e1] and a footnote[^1]. + +Another paragraph with another endnote[^e2] and another footnote[^2]. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^e1]: First endnote. + +[^e2]: Second endnote. +^D +A paragraph with an endnote[[^1]]{.endnote} and a footnote[^2]. + +Another paragraph with another endnote[[^3]]{.endnote} and another +footnote[^4]. + +[^1]: First endnote. + +[^2]: First footnote. + +[^3]: Second endnote. + +[^4]: Second footnote. +``` +``` +% pandoc -f markdown -t markdown +A paragraph with an endnote[^EN1] and a footnote[^1]. + +Another paragraph with another endnote[^EN2] and another footnote[^2]. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^EN1]: First endnote. + +[^EN2]: Second endnote. +^D +A paragraph with an endnote[^1] and a footnote[^2]. + +Another paragraph with another endnote[^3] and another footnote[^4]. + +[^1]: First endnote. + +[^2]: First footnote. + +[^3]: Second endnote. + +[^4]: Second footnote. +``` From 79736984c60e9f7f531b2c80f17e88e18dd66376 Mon Sep 17 00:00:00 2001 From: massifrg Date: Fri, 6 Mar 2026 19:11:32 +0100 Subject: [PATCH 08/11] Markdown writer: endnotes extension When the endnotes extension is active, Notes inside Span of class "endnote" are treated as endnotes: - their refs are prefixed with "EN" or the prefix set with --endnotes-prefix - they are listed together after all the normal notes (footnotes) --- src/Text/Pandoc/Writers/Markdown.hs | 24 ++++++++++++++++------ src/Text/Pandoc/Writers/Markdown/Inline.hs | 22 ++++++++++++++++---- src/Text/Pandoc/Writers/Markdown/Types.hs | 2 ++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Text/Pandoc/Writers/Markdown.hs b/src/Text/Pandoc/Writers/Markdown.hs index 11d8c39d76ea..c49760d10b0c 100644 --- a/src/Text/Pandoc/Writers/Markdown.hs +++ b/src/Text/Pandoc/Writers/Markdown.hs @@ -287,15 +287,24 @@ keyToMarkdown opts (label', (src, tit), attr) = do notesToMarkdown :: PandocMonad m => WriterOptions -> [[Block]] -> MD m (Doc Text) notesToMarkdown opts notes = do n <- gets stNoteNum - notes' <- zipWithM (noteToMarkdown opts) [n..] notes + notes' <- zipWithM (noteToMarkdown opts "") [n..] notes modify $ \st -> st { stNoteNum = stNoteNum st + length notes } return $ vsep notes' +-- | Return markdown representation of endnotes when Ext_endnotes is enabled. +endnotesToMarkdown :: PandocMonad m => WriterOptions -> [[Block]] -> MD m (Doc Text) +endnotesToMarkdown opts notes = do + n <- gets stEndnoteNum + let prefix = writerEndnotesPrefix opts + notes' <- zipWithM (noteToMarkdown opts prefix) [n..] notes + modify $ \st -> st { stEndnoteNum = stEndnoteNum st + length notes } + return $ vsep notes' + -- | Return markdown representation of a note. -noteToMarkdown :: PandocMonad m => WriterOptions -> Int -> [Block] -> MD m (Doc Text) -noteToMarkdown opts num blocks = do +noteToMarkdown :: PandocMonad m => WriterOptions -> Text -> Int -> [Block] -> MD m (Doc Text) +noteToMarkdown opts prefix num blocks = do contents <- blockListToMarkdown opts blocks - let num' = literal $ writerIdentifierPrefix opts <> tshow num + let num' = literal $ prefix <> writerIdentifierPrefix opts <> tshow num let marker = if isEnabled Ext_footnotes opts then literal "[^" <> num' <> literal "]:" else literal "[" <> num' <> literal "]" @@ -337,17 +346,20 @@ notesAndRefs :: PandocMonad m => WriterOptions -> MD m (Doc Text) notesAndRefs opts = do notes' <- gets stNotes >>= notesToMarkdown opts . reverse modify $ \s -> s { stNotes = [] } + notes'' <- gets stEndnotes >>= endnotesToMarkdown opts . reverse + modify $ \s -> s { stEndnotes = [] } + let allNotes = notes' <> (if isEmpty notes' then empty else blankline) <> notes'' refs' <- gets stRefs >>= refsToMarkdown opts . reverse modify $ \s -> s { stPrevRefs = stPrevRefs s ++ stRefs s , stRefs = []} let endSpacing = if | writerReferenceLocation opts == EndOfDocument -> empty - | isEmpty notes' && isEmpty refs' -> empty + | isEmpty allNotes && isEmpty refs' -> empty | otherwise -> blankline return $ - (if isEmpty notes' then empty else blankline <> notes') <> + (if isEmpty allNotes then empty else blankline <> allNotes) <> (if isEmpty refs' then empty else blankline <> refs') <> endSpacing diff --git a/src/Text/Pandoc/Writers/Markdown/Inline.hs b/src/Text/Pandoc/Writers/Markdown/Inline.hs index 9b2c5842aef2..eb64157c95cf 100644 --- a/src/Text/Pandoc/Writers/Markdown/Inline.hs +++ b/src/Text/Pandoc/Writers/Markdown/Inline.hs @@ -335,6 +335,11 @@ avoidBadWraps inListItem = go . toList toList (Concat a b) = a : toList b toList x = [x] +-- check if the Attr of a Span makes the embedded Note an endnote +isEndnoteSpan :: WriterOptions -> Attr -> Bool +isEndnoteSpan opts (_, ["endnote"], _) = isEnabled Ext_endnotes opts +isEndnoteSpan _ _ = False + -- | Convert Pandoc inline element to markdown. inlineToMarkdown :: PandocMonad m => WriterOptions -> Inline -> MD m (Doc Text) inlineToMarkdown opts (Span ("",["emoji"],kvs) [Str s]) = @@ -347,8 +352,10 @@ inlineToMarkdown opts (Span ("",["mark"],[]) ils) = do contents <- inlineListToMarkdown opts ils return $ "==" <> contents <> "==" inlineToMarkdown opts (Span attrs ils) = do + modify (\s -> s {stInEndnote = isEndnoteSpan opts attrs}) variant <- asks envVariant contents <- inlineListToMarkdown opts ils + modify (\s -> s {stInEndnote = False}) return $ case attrs of (_,["csl-block"],_) -> (cr <>) (_,["csl-left-margin"],_) -> (cr <>) @@ -723,12 +730,19 @@ inlineToMarkdown opts img@(Image attr alternate (source, tit)) literal source <> ")" <> cr _ -> "!" <> linkPart inlineToMarkdown opts (Note contents) = do - modify (\st -> st{ stNotes = contents : stNotes st }) + inEndnote <- stInEndnote <$> get + if inEndnote + then modify (\st -> st{ stEndnotes = contents : stEndnotes st }) + else modify (\st -> st{ stNotes = contents : stNotes st }) st <- get - let ref = literal $ writerIdentifierPrefix opts <> tshow (stNoteNum st + length (stNotes st) - 1) + let ref = if inEndnote + then literal $ writerEndnotesPrefix opts + <> writerIdentifierPrefix opts + <> tshow (stEndnoteNum st + length (stEndnotes st) - 1) + else literal $ writerIdentifierPrefix opts <> tshow (stNoteNum st + length (stNotes st) - 1) if isEnabled Ext_footnotes opts - then return $ "[^" <> ref <> "]" - else return $ "[" <> ref <> "]" + then return $ "[^" <> ref <> "]" + else return $ "[" <> ref <> "]" makeMathPlainer :: [Inline] -> [Inline] makeMathPlainer = walk go diff --git a/src/Text/Pandoc/Writers/Markdown/Types.hs b/src/Text/Pandoc/Writers/Markdown/Types.hs index af84eedeb635..bbdb66297256 100644 --- a/src/Text/Pandoc/Writers/Markdown/Types.hs +++ b/src/Text/Pandoc/Writers/Markdown/Types.hs @@ -66,6 +66,7 @@ data WriterState = WriterState { stNotes :: Notes , stLastIdx :: Int , stIds :: Set.Set Text , stNoteNum :: Int + , stInEndnote :: Bool , stEndnotes :: Notes , stEndnoteNum :: Int } @@ -78,6 +79,7 @@ instance Default WriterState , stLastIdx = 0 , stIds = Set.empty , stNoteNum = 1 + , stInEndnote = False , stEndnotes = [] , stEndnoteNum = 1 } From 580477f37f9f89c6a9063a15fe56dd96c443219b Mon Sep 17 00:00:00 2001 From: massifrg Date: Fri, 6 Mar 2026 23:53:42 +0100 Subject: [PATCH 09/11] Markdown writer: endnotes support When the "endnotes" extension is enabled, the endnotes (Notes wrapped in a Span of class "endnote") are written with refs that start with the prefix set with the --endnotes-prefix option. Moreover, notes' texts are written in a different way: first all the footnotes, then all the endnotes. --- src/Text/Pandoc/Writers/Markdown/Inline.hs | 14 +++-- test/command/11503.md | 60 ++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Text/Pandoc/Writers/Markdown/Inline.hs b/src/Text/Pandoc/Writers/Markdown/Inline.hs index eb64157c95cf..a6a5cc063ce7 100644 --- a/src/Text/Pandoc/Writers/Markdown/Inline.hs +++ b/src/Text/Pandoc/Writers/Markdown/Inline.hs @@ -340,6 +340,11 @@ isEndnoteSpan :: WriterOptions -> Attr -> Bool isEndnoteSpan opts (_, ["endnote"], _) = isEnabled Ext_endnotes opts isEndnoteSpan _ _ = False +attrWithoutEndnoteClass :: WriterOptions -> Attr -> Attr +attrWithoutEndnoteClass opts attr = case (attr, isEnabled Ext_endnotes opts) of + ((ident, ["endnote"], attributes), True) -> (ident, [], attributes) + _ -> attr + -- | Convert Pandoc inline element to markdown. inlineToMarkdown :: PandocMonad m => WriterOptions -> Inline -> MD m (Doc Text) inlineToMarkdown opts (Span ("",["emoji"],kvs) [Str s]) = @@ -364,12 +369,13 @@ inlineToMarkdown opts (Span attrs ils) = do $ case variant of PlainText -> contents Markua -> "`" <> contents <> "`" <> attrsToMarkua opts attrs - _ | attrs == nullAttr -> contents + _ | nullAttr == attrWithoutEndnoteClass opts attrs -> contents | isEnabled Ext_bracketed_spans opts -> - let attrs' = if attrs /= nullAttr - then attrsToMarkdown opts attrs + let attrs' = attrWithoutEndnoteClass opts attrs + attrs'' = if attrs' /= nullAttr + then attrsToMarkdown opts attrs' else empty - in "[" <> contents <> "]" <> attrs' + in "[" <> contents <> "]" <> attrs'' | isEnabled Ext_raw_html opts || isEnabled Ext_native_spans opts -> tagWithAttrs "span" attrs <> contents <> literal "" diff --git a/test/command/11503.md b/test/command/11503.md index eccce3c97cee..a8419dc8a340 100644 --- a/test/command/11503.md +++ b/test/command/11503.md @@ -78,3 +78,63 @@ Another paragraph with another endnote[^3] and another footnote[^4]. [^4]: Second footnote. ``` +``` +% pandoc -f markdown+endnotes -t markdown+endnotes +# Roundtrip test + +A paragraph with an endnote[^EN1] and a footnote[^1]. + +Another paragraph with another endnote[^EN2] and another footnote[^2]. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^EN1]: First endnote. + +[^EN2]: Second endnote. +^D +# Roundtrip test + +A paragraph with an endnote[^EN1] and a footnote[^1]. + +Another paragraph with another endnote[^EN2] and another footnote[^2]. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^EN1]: First endnote. + +[^EN2]: Second endnote. +``` +``` +% pandoc --endnotes-prefix=e -f markdown+endnotes -t markdown+endnotes +# Roundtrip test with a different endnotes prefix. + +A paragraph with an endnote[^e1] and a footnote[^1]. + +Another paragraph with another endnote[^e2] and another footnote[^2]. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^e1]: First endnote. + +[^e2]: Second endnote. +^D +# Roundtrip test with a different endnotes prefix. + +A paragraph with an endnote[^e1] and a footnote[^1]. + +Another paragraph with another endnote[^e2] and another footnote[^2]. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^e1]: First endnote. + +[^e2]: Second endnote. +``` \ No newline at end of file From a407f0e0ab5db6d5d79db25a23e267a89f43a1ff Mon Sep 17 00:00:00 2001 From: massifrg Date: Fri, 6 Mar 2026 23:54:03 +0100 Subject: [PATCH 10/11] MANUAL: endnotes documentation --- MANUAL.txt | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/MANUAL.txt b/MANUAL.txt index 716ffb199a28..cdeb44428abb 100644 --- a/MANUAL.txt +++ b/MANUAL.txt @@ -1268,6 +1268,16 @@ header when requesting a document from a URL: and Haddock output. This is useful for preventing duplicate identifiers when generating fragments to be included in other pages. +`--endnotes-prefix=`*STRING* + +: It's used with Markdown, only when the `endnotes` extension is enabled. + The Markdown reader uses it to discriminate between footnotes (normal + notes) and endnotes, which internally are coded as notes embedded in a + `Span` with the class "endnote". + It's also used by the Markdown writer as a prefix for all the endnotes' + references. + Its default value is "EN". + `-T` *STRING*, `--title-prefix=`*STRING* : Specify *STRING* as a prefix at the beginning of the title @@ -5923,6 +5933,59 @@ they cannot contain multiple paragraphs). The syntax is as follows: Inline and regular footnotes may be mixed freely. +## Endnotes + +You can use the following convention to specify endnotes: surround a +footnote with a `Span` of class "endnote", like this: + + Here' and endnote[[^1]]{.endnote}. + + [^1]: This is the endnote text. + +When a reader or a writer does not know about this convention, those +notes are just regular footnotes, just with a transparent `Span` wrapper +around them. + +### Extension: `endnotes` ### + +Enabling this extension tells some readers and writers to use that +convention to distinguish endnotes from footnotes. + +The markdown reader lets you specify endnotes in a lighter way: + + Here' and endnote[^EN1]. + + [^EN1]: This is the endnote text. + +because "EN" is the default prefix for endnotes; when the reader meets +a note reference starting with "EN", it wraps the note in a `Span` with +the "endnote" class. +That prefix can be set with the `--endnotes-prefix` option. + +The markdown writer prepends the endnotes' references with that prefix. +Moreover, it lists all the footnotes' texts followed by the endnotes' +texts at the end of the document: + + A document with an endnote[[^1]]{.endnote}, a footnote[^2], + and another endnote[[^3]]{.endnote}. + + [^1]: First endnote. + + [^2]: First footnote. + + [^3]: Second endnote. + +When you convert it with `-t markdown+endnotes` it becomes: + + A document with an endnote[^EN1], a footnote[^1], + and another endnote[^EN2]. + + [^1]: First footnote. + + [^EN1]: First endnote. + + [^EN2]: Second endnote. + ## Citation syntax ### Extension: `citations` ### From e7d5a03ba59f63ac06afd5caff43047fd0993825 Mon Sep 17 00:00:00 2001 From: massifrg Date: Mon, 9 Mar 2026 20:46:51 +0100 Subject: [PATCH 11/11] Markdown writer: fix endnotes When a Span has the "endnote" class but it does not contain only a Note, the "endnote" class is not suppressed when endnotes extension is enabled. --- src/Text/Pandoc/Writers/Markdown/Inline.hs | 10 ++++---- test/command/11503.md | 30 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Text/Pandoc/Writers/Markdown/Inline.hs b/src/Text/Pandoc/Writers/Markdown/Inline.hs index a6a5cc063ce7..3db12a0e62c3 100644 --- a/src/Text/Pandoc/Writers/Markdown/Inline.hs +++ b/src/Text/Pandoc/Writers/Markdown/Inline.hs @@ -340,9 +340,9 @@ isEndnoteSpan :: WriterOptions -> Attr -> Bool isEndnoteSpan opts (_, ["endnote"], _) = isEnabled Ext_endnotes opts isEndnoteSpan _ _ = False -attrWithoutEndnoteClass :: WriterOptions -> Attr -> Attr -attrWithoutEndnoteClass opts attr = case (attr, isEnabled Ext_endnotes opts) of - ((ident, ["endnote"], attributes), True) -> (ident, [], attributes) +attrWithoutEndnoteClass :: WriterOptions -> Attr -> [Inline] -> Attr +attrWithoutEndnoteClass opts attr ils = case (attr, isEnabled Ext_endnotes opts, ils) of + ((ident, ["endnote"], attributes), True, [Note _]) -> (ident, [], attributes) _ -> attr -- | Convert Pandoc inline element to markdown. @@ -369,9 +369,9 @@ inlineToMarkdown opts (Span attrs ils) = do $ case variant of PlainText -> contents Markua -> "`" <> contents <> "`" <> attrsToMarkua opts attrs - _ | nullAttr == attrWithoutEndnoteClass opts attrs -> contents + _ | nullAttr == attrWithoutEndnoteClass opts attrs ils -> contents | isEnabled Ext_bracketed_spans opts -> - let attrs' = attrWithoutEndnoteClass opts attrs + let attrs' = attrWithoutEndnoteClass opts attrs ils attrs'' = if attrs' /= nullAttr then attrsToMarkdown opts attrs' else empty diff --git a/test/command/11503.md b/test/command/11503.md index a8419dc8a340..75520d457185 100644 --- a/test/command/11503.md +++ b/test/command/11503.md @@ -137,4 +137,34 @@ Another paragraph with another endnote[^e2] and another footnote[^2]. [^e1]: First endnote. [^e2]: Second endnote. +``` +``` +% pandoc -f markdown -t markdown +Normal [spans with an *endnote* class]{.endnote} should stay untouched. +^D +Normal [spans with an *endnote* class]{.endnote} should stay untouched. +``` +``` +% pandoc -f markdown+endnotes -t markdown +Normal [spans with an *endnote* class]{.endnote} should stay untouched, +even with endnotes enabled on the reader. +^D +Normal [spans with an *endnote* class]{.endnote} should stay untouched, +even with endnotes enabled on the reader. +``` +``` +% pandoc -f markdown -t markdown+endnotes +Normal [spans with an *endnote* class]{.endnote} should stay untouched, +even with endnotes enabled on the writer. +^D +Normal [spans with an *endnote* class]{.endnote} should stay untouched, +even with endnotes enabled on the writer. +``` +``` +% pandoc -f markdown+endnotes -t markdown+endnotes +Normal [spans with an *endnote* class]{.endnote} should stay untouched, +even with endnotes enabled both on the reader and the writer. +^D +Normal [spans with an *endnote* class]{.endnote} should stay untouched, +even with endnotes enabled both on the reader and the writer. ``` \ No newline at end of file