From 7c207a2447d63c9008b287a46afae1403387950c Mon Sep 17 00:00:00 2001 From: massifrg Date: Wed, 4 Mar 2026 13:31:43 +0100 Subject: [PATCH 01/12] DOCX writer: support for endnotes Initial support for endnotes for openxml. You get an endnote embedding a Note in a Span that has an "endnote" class. --- data/docx/[Content_Types].xml | 2 +- data/docx/word/endnotes.xml | 21 ++++++++++++++ data/docx/word/settings.xml | 4 +++ data/docx/word/styles.xml | 15 ++++++++++ src/Text/Pandoc/Writers/Docx.hs | 32 +++++++++++++++++++-- src/Text/Pandoc/Writers/Docx/OpenXML.hs | 37 ++++++++++++++++++------- src/Text/Pandoc/Writers/Docx/Types.hs | 8 ++++++ 7 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 data/docx/word/endnotes.xml diff --git a/data/docx/[Content_Types].xml b/data/docx/[Content_Types].xml index 0c0118a88b37..0219fc66b6e2 100644 --- a/data/docx/[Content_Types].xml +++ b/data/docx/[Content_Types].xml @@ -1,2 +1,2 @@ - + diff --git a/data/docx/word/endnotes.xml b/data/docx/word/endnotes.xml new file mode 100644 index 000000000000..a9bb2c2174c1 --- /dev/null +++ b/data/docx/word/endnotes.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + Endnote Text. + + + + Endnote Block Text + + + diff --git a/data/docx/word/settings.xml b/data/docx/word/settings.xml index 19bd52257d8a..26136d89cb4d 100644 --- a/data/docx/word/settings.xml +++ b/data/docx/word/settings.xml @@ -16,6 +16,10 @@ + + + + diff --git a/data/docx/word/styles.xml b/data/docx/word/styles.xml index effc926c4a2e..5d2436ccc166 100644 --- a/data/docx/word/styles.xml +++ b/data/docx/word/styles.xml @@ -583,6 +583,14 @@ + + + + + + + + @@ -686,6 +694,13 @@ + + + + + + + diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs index 782833965591..b15010390cdd 100644 --- a/src/Text/Pandoc/Writers/Docx.hs +++ b/src/Text/Pandoc/Writers/Docx.hs @@ -123,7 +123,7 @@ writeDocx opts doc = do [ mknode "w:numRestart" [("w:val","eachSect")] () ] ] - ((contents, footnotes, comments), st) <- runStateT + ((contents, footnotes, endnotes, comments), st) <- runStateT (runReaderT (writeOpenXML opts{ writerWrapText = WrapNone } doc') @@ -143,6 +143,8 @@ writeDocx opts doc = do (BL.fromStrict $ UTF8.fromText contents) let footnotesEntry = mkFootnotesEntry epochtime footnotes let footnoteRelEntry = mkFootnoteRelsEntry epochtime (stExternalLinks st) + let endnotesEntry = mkEndnotesEntry epochtime endnotes + let endnoteRelEntry = mkEndnoteRelsEntry epochtime (stExternalLinks st) let commentsEntry = mkCommentsEntry epochtime comments let styleEntry = mkStylesEntry epochtime styledoc styleMaps st opts numEntry <- mkNumberingEntry refArchive distArchive epochtime (stLists st) @@ -165,6 +167,7 @@ writeDocx opts doc = do let archive = foldr addEntryToArchive emptyArchive $ contentTypesEntry : relsEntry : contentEntry : relEntry : footnoteRelEntry : numEntry : styleEntry : footnotesEntry : + endnoteRelEntry : endnotesEntry : commentsEntry : docPropsEntry : customPropsEntry : settingsEntry : @@ -569,6 +572,8 @@ extractRelationships refArchive distArchive = do "theme/theme1.xml") ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", "footnotes.xml") + ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", + "endnotes.xml") ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", "comments.xml") ] @@ -595,6 +600,26 @@ mkFootnoteRelsEntry epochtime externalLinks = [("xmlns","http://schemas.openxmlformats.org/package/2006/relationships")] linkrels +-- | Create endnotes XML entry +mkEndnotesEntry :: Integer -> [Element] -> Entry +mkEndnotesEntry epochtime endnotes = + let notes = mknode "w:endnotes" stdAttributes endnotes + in toEntry "word/endnotes.xml" epochtime $ renderXml notes + +-- | Create endnote relationships entry +mkEndnoteRelsEntry :: Integer -> M.Map Text Text -> Entry +mkEndnoteRelsEntry epochtime externalLinks = + let linkrels = map toLinkRel $ M.toList externalLinks + toLinkRel (src, ident) = mknode "Relationship" + [("Type","http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink") + ,("Id",ident) + ,("Target",src) + ,("TargetMode","External")] () + in toEntry "word/_rels/endnotes.xml.rels" epochtime + $ renderXml $ mknode "Relationships" + [("xmlns","http://schemas.openxmlformats.org/package/2006/relationships")] + linkrels + -- | Create comments XML entry mkCommentsEntry :: Integer -> [Element] -> Entry mkCommentsEntry epochtime comments = @@ -663,6 +688,8 @@ mkContentTypesEntry epochtime imgs headers footers refArchive = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml") ,("/word/footnotes.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml") + ,("/word/endnotes.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml") ] ++ map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml")) headers ++ @@ -836,7 +863,8 @@ collectReferenceEntries refArchive distArchive headers footers = do , "word/_rels/" `isPrefixOf` eRelativePath e , ".xml.rels" `isSuffixOf` eRelativePath e , eRelativePath e /= "word/_rels/document.xml.rels" - , eRelativePath e /= "word/_rels/footnotes.xml.rels" ] + , eRelativePath e /= "word/_rels/footnotes.xml.rels" + , eRelativePath e /= "word/_rels/endnotes.xml.rels" ] let otherMediaEntries = [ e | e <- zEntries refArchive , "word/media/" `isPrefixOf` eRelativePath e ] return $ docPropsAppEntry : themeEntry : fontTableEntry : webSettingsEntry diff --git a/src/Text/Pandoc/Writers/Docx/OpenXML.hs b/src/Text/Pandoc/Writers/Docx/OpenXML.hs index 5490a732de52..cbff8cb1999a 100644 --- a/src/Text/Pandoc/Writers/Docx/OpenXML.hs +++ b/src/Text/Pandoc/Writers/Docx/OpenXML.hs @@ -229,11 +229,11 @@ sectionSeparator = do Nothing -> pure Nothing --- | Convert Pandoc document to rendered document contents plus two lists of --- OpenXML elements (footnotes and comments). +-- | Convert Pandoc document to rendered document contents plus three lists of +-- OpenXML elements (footnotes, endnotes and comments). writeOpenXML :: PandocMonad m => WriterOptions -> Pandoc - -> WS m (Text, [Element], [Element]) + -> WS m (Text, [Element], [Element], [Element]) writeOpenXML opts (Pandoc meta blocks) = do setupTranslations meta let includeTOC = writerTableOfContents opts || lookupMetaBool "toc" meta @@ -264,6 +264,7 @@ writeOpenXML opts (Pandoc meta blocks) = do doc' <- setFirstPara >> blocksToOpenXML opts blocks let body = vcat $ map (literal . showContent) doc' notes' <- gets (reverse . stFootnotes) + endnotes' <- gets (reverse . stEndnotes) comments <- gets (reverse . stComments) let toComment (kvs, ils) = do annotation <- inlinesToOpenXML opts ils @@ -322,7 +323,7 @@ writeOpenXML opts (Pandoc meta blocks) = do $ metadata tpl <- maybe (lift $ compileDefaultTemplate "openxml") pure $ writerTemplate opts let rendered = render Nothing $ renderTemplate tpl context - return (rendered, notes', comments') + return (rendered, notes', endnotes', comments') -- | Convert a list of Pandoc blocks to OpenXML. blocksToOpenXML :: (PandocMonad m) => WriterOptions -> [Block] -> WS m [Content] @@ -755,6 +756,11 @@ inlineToOpenXML' opts SoftBreak = inlineToOpenXML opts (Str " ") inlineToOpenXML' opts (Span ("",["mark"],[]) ils) = withTextProp (mknode "w:highlight" [("w:val","yellow")] ()) $ inlinesToOpenXML opts ils +inlineToOpenXML' opts (Span ("",["endnote"],[]) ils) = do + modify $ \s -> s { stInEndnote = True } + endnote <- inlinesToOpenXML opts ils + modify $ \s -> s { stInEndnote = False } + return endnote inlineToOpenXML' opts (Span ("",["csl-block"],[]) ils) = inlinesToOpenXML opts ils inlineToOpenXML' opts (Span ("",["csl-left-margin"],[]) ils) = @@ -905,28 +911,39 @@ inlineToOpenXML' opts (Code attrs str) = do Skylighting _ -> highlighted _ -> unhighlighted inlineToOpenXML' opts (Note bs) = do + isEndnote <- gets stInEndnote notes <- gets stFootnotes notenum <- getUniqueId - footnoteStyle <- rStyleM "Footnote Reference" + footnoteStyle <- rStyleM $ if isEndnote + then "Endnote Reference" + else "Footnote Reference" + let noteRefNodeName = if isEndnote then "w:endnoteRef" else "w:footnoteRef" let notemarker = mknode "w:r" [] [ mknode "w:rPr" [] footnoteStyle - , mknode "w:footnoteRef" [] () ] + , mknode noteRefNodeName [] () ] let notemarkerXml = RawInline (Format "openxml") $ ppElement notemarker let insertNoteRef (Plain ils : xs) = Plain (notemarkerXml : Space : ils) : xs insertNoteRef (Para ils : xs) = Para (notemarkerXml : Space : ils) : xs insertNoteRef xs = Para [notemarkerXml] : xs + let noteTextStyleName = if isEndnote then "Endnote Text" else "Footnote Text" contents <- local (\env -> env{ envListLevel = -1 , envParaProperties = mempty , envTextProperties = mempty , envInNote = True }) - (withParaPropM (pStyleM "Footnote Text") $ + (withParaPropM (pStyleM noteTextStyleName) $ blocksToOpenXML opts $ insertNoteRef bs) - let newnote = mknode "w:footnote" [("w:id", notenum)] contents - modify $ \s -> s{ stFootnotes = newnote : notes } + let noteNodeName = if isEndnote then "w:endnote" else "w:footnote" + let newnote = mknode noteNodeName [("w:id", notenum)] contents + modify $ \s -> if isEndnote + then s{ stEndnotes = newnote : notes } + else s{ stFootnotes = newnote : notes } + let noteReferenceNodeName = if isEndnote + then "w:endnoteReference" + else "w:footnoteReference" return [ Elem $ mknode "w:r" [] [ mknode "w:rPr" [] footnoteStyle - , mknode "w:footnoteReference" [("w:id", notenum)] () ] ] + , mknode noteReferenceNodeName [("w:id", notenum)] () ] ] -- internal link: inlineToOpenXML' opts (Link _ txt (T.uncons -> Just ('#', xs),_)) = do contents <- withTextPropM (rStyleM "Hyperlink") $ inlinesToOpenXML opts txt diff --git a/src/Text/Pandoc/Writers/Docx/Types.hs b/src/Text/Pandoc/Writers/Docx/Types.hs index febef6603dd3..a2a199e8c2b6 100644 --- a/src/Text/Pandoc/Writers/Docx/Types.hs +++ b/src/Text/Pandoc/Writers/Docx/Types.hs @@ -111,6 +111,7 @@ defaultWriterEnv = WriterEnv data WriterState = WriterState{ stFootnotes :: [Element] + , stEndnotes :: [Element] , stComments :: [([(Text, Text)], [Inline])] , stSectionIds :: Set.Set Text , stExternalLinks :: M.Map Text Text @@ -126,6 +127,7 @@ data WriterState = WriterState{ -- Should only be used once, for the first paragraph. , stInTable :: Bool , stInList :: Bool + , stInEndnote :: Bool , stTocTitle :: [Inline] , stDynamicParaProps :: Set.Set ParaStyleName , stDynamicTextProps :: Set.Set CharStyleName @@ -137,6 +139,7 @@ data WriterState = WriterState{ defaultWriterState :: WriterState defaultWriterState = WriterState{ stFootnotes = defaultFootnotes + , stEndnotes = defaultEndnotes , stComments = [] , stSectionIds = Set.empty , stExternalLinks = M.empty @@ -151,6 +154,7 @@ defaultWriterState = WriterState{ , stNumIdUsed = False , stInTable = False , stInList = False + , stInEndnote = False , stTocTitle = [Str "Table of Contents"] , stDynamicParaProps = Set.empty , stDynamicTextProps = Set.empty @@ -180,6 +184,10 @@ defaultFootnotes = [ mknode "w:footnote" [ mknode "w:r" [] [ mknode "w:continuationSeparator" [] ()]]]] +-- TODO: verify whether Word behaves the same with endnotes as it does with footnotes. +defaultEndnotes :: [Element] +defaultEndnotes = [] + pStyleM :: (PandocMonad m) => ParaStyleName -> WS m XML.Element pStyleM styleName = do pStyleMap <- gets (smParaStyle . stStyleMaps) From f3bb8bba3617e7bf75ae2205e8b3ce4402187c0d Mon Sep 17 00:00:00 2001 From: massifrg Date: Wed, 4 Mar 2026 17:34:44 +0100 Subject: [PATCH 02/12] DOCX: extension "endnotes" added A new extension "endnotes" to enable endnotes in docx format --- pandoc-lua-engine/test/lua/module/pandoc-format.lua | 1 + src/Text/Pandoc/Extensions.hs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pandoc-lua-engine/test/lua/module/pandoc-format.lua b/pandoc-lua-engine/test/lua/module/pandoc-format.lua index 6a1915a163f7..d2a10b94e675 100644 --- a/pandoc-lua-engine/test/lua/module/pandoc-format.lua +++ b/pandoc-lua-engine/test/lua/module/pandoc-format.lua @@ -24,6 +24,7 @@ return { 'citations', 'east_asian_line_breaks', 'empty_paragraphs', + 'endnotes', 'gfm_auto_identifiers', 'native_numbering', 'styles', diff --git a/src/Text/Pandoc/Extensions.hs b/src/Text/Pandoc/Extensions.hs index 7fcb214cdbf4..82dadb36ac3b 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 @@ -532,6 +533,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 7a469963eb0b2b1ce3e3082f3f39cea4d69025e2 Mon Sep 17 00:00:00 2001 From: massifrg Date: Wed, 4 Mar 2026 22:11:47 +0100 Subject: [PATCH 03/12] DOCX writer: endnotes ext working The "endnotes" extension for docx is working with the docx Writer. No tests for this feature yet, but old tests now pass. --- data/docx/[Content_Types].xml | 2 +- data/docx/word/styles.xml | 15 ---------- src/Text/Pandoc/Writers/Docx.hs | 38 ++++++++++++++----------- src/Text/Pandoc/Writers/Docx/OpenXML.hs | 20 +++++++------ src/Text/Pandoc/Writers/Docx/Types.hs | 13 ++++++++- 5 files changed, 47 insertions(+), 41 deletions(-) diff --git a/data/docx/[Content_Types].xml b/data/docx/[Content_Types].xml index 0219fc66b6e2..0c0118a88b37 100644 --- a/data/docx/[Content_Types].xml +++ b/data/docx/[Content_Types].xml @@ -1,2 +1,2 @@ - + diff --git a/data/docx/word/styles.xml b/data/docx/word/styles.xml index 5d2436ccc166..effc926c4a2e 100644 --- a/data/docx/word/styles.xml +++ b/data/docx/word/styles.xml @@ -583,14 +583,6 @@ - - - - - - - - @@ -694,13 +686,6 @@ - - - - - - - diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs index b15010390cdd..6f6a8f87e9a4 100644 --- a/src/Text/Pandoc/Writers/Docx.hs +++ b/src/Text/Pandoc/Writers/Docx.hs @@ -105,7 +105,7 @@ writeDocx opts doc = do } -- Phase 5: Relationship extraction - (baserels, headers, footers, newMaxRelId) <- extractRelationships refArchive distArchive + (baserels, headers, footers, newMaxRelId) <- extractRelationships opts refArchive distArchive let initialSt = defaultWriterState { stStyleMaps = styleMaps @@ -137,7 +137,7 @@ writeDocx opts doc = do -- because Word sometimes changes these files when a reference.docx is modified, -- e.g. deleting the reference to footnotes.xml or removing default entries -- for image content types. - let contentTypesEntry = mkContentTypesEntry epochtime imgs headers footers refArchive + let contentTypesEntry = mkContentTypesEntry epochtime imgs headers footers endnotes refArchive let relEntry = mkDocumentRelsEntry epochtime baserels imgs (stExternalLinks st) let contentEntry = toEntry "word/document.xml" epochtime (BL.fromStrict $ UTF8.fromText contents) @@ -167,11 +167,12 @@ writeDocx opts doc = do let archive = foldr addEntryToArchive emptyArchive $ contentTypesEntry : relsEntry : contentEntry : relEntry : footnoteRelEntry : numEntry : styleEntry : footnotesEntry : - endnoteRelEntry : endnotesEntry : - commentsEntry : - docPropsEntry : customPropsEntry : - settingsEntry : - imageEntries ++ refEntries + commentsEntry : docPropsEntry : customPropsEntry : settingsEntry : + imageEntries + ++ refEntries + ++ if (isEnabled Ext_endnotes opts) + then [endnoteRelEntry, endnotesEntry] + else [] return $ fromArchive archive newParaPropToOpenXml :: ParaStyleName -> Element @@ -528,9 +529,9 @@ extractPageLayout refArchive distArchive = do -- | Parse and augment relationships from reference.docx extractRelationships :: PandocMonad m - => Archive -> Archive + => WriterOptions -> Archive -> Archive -> m ([Element], [Element], [Element], Int) -extractRelationships refArchive distArchive = do +extractRelationships opts refArchive distArchive = do let isImageNode e = findAttr (QName "Type" Nothing Nothing) e == Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" let isHeaderNode e = findAttr (QName "Type" Nothing Nothing) e == Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" let isFooterNode e = findAttr (QName "Type" Nothing Nothing) e == Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" @@ -557,7 +558,7 @@ extractRelationships refArchive distArchive = do ,("Target",target')] () : rels) _ -> (maxId, rels) - let (newMaxRelId, baserels) = foldr addBaseRel (maxRelId, parsedRels) + let (newMaxRelId, baserels) = foldr addBaseRel (maxRelId, parsedRels) $ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", "numbering.xml") ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", @@ -572,11 +573,10 @@ extractRelationships refArchive distArchive = do "theme/theme1.xml") ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", "footnotes.xml") - ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", - "endnotes.xml") ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", "comments.xml") - ] + ] ++ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", + "endnotes.xml") | isEnabled Ext_endnotes opts] return (baserels, headers, footers, newMaxRelId) @@ -652,9 +652,10 @@ mkContentTypesEntry :: Integer -> [(String, String, Maybe MimeType, B.ByteString)] -- imgs -> [Element] -- headers -> [Element] -- footers + -> [Element] -- endnotes -> Archive -- refArchive -> Entry -mkContentTypesEntry epochtime imgs headers footers refArchive = +mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = let mkOverrideNode (part', contentType') = mknode "Override" [("PartName", T.pack part') ,("ContentType", contentType')] () @@ -663,6 +664,11 @@ mkContentTypesEntry epochtime imgs headers footers refArchive = fromMaybe "application/octet-stream" mbMimeType) mkMediaOverride imgpath = mkOverrideNode ("/" <> imgpath, getMimeTypeDef imgpath) + endnotesOverride = if null endnotes + then [("/word/endnotes.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml")] + else [] + overrides = map mkOverrideNode ( [("/word/webSettings.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml") @@ -688,9 +694,8 @@ mkContentTypesEntry epochtime imgs headers footers refArchive = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml") ,("/word/footnotes.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml") - ,("/word/endnotes.xml", - "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml") ] ++ + endnotesOverride ++ map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml")) headers ++ map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), @@ -750,6 +755,7 @@ mkStylesEntry epochtime styledoc styleMaps st opts = (\sty -> not $ hasStyleName sty $ smCharStyle styleMaps) (Set.toList $ stDynamicTextProps st) + -- TODO: add styles for endnotes, when Ext_endnotes is enabled newstyles = map newParaPropToOpenXml newDynamicParaProps ++ map newTextPropToOpenXml newDynamicTextProps ++ (case writerHighlightMethod opts of diff --git a/src/Text/Pandoc/Writers/Docx/OpenXML.hs b/src/Text/Pandoc/Writers/Docx/OpenXML.hs index cbff8cb1999a..63e5029da611 100644 --- a/src/Text/Pandoc/Writers/Docx/OpenXML.hs +++ b/src/Text/Pandoc/Writers/Docx/OpenXML.hs @@ -756,11 +756,13 @@ inlineToOpenXML' opts SoftBreak = inlineToOpenXML opts (Str " ") inlineToOpenXML' opts (Span ("",["mark"],[]) ils) = withTextProp (mknode "w:highlight" [("w:val","yellow")] ()) $ inlinesToOpenXML opts ils -inlineToOpenXML' opts (Span ("",["endnote"],[]) ils) = do - modify $ \s -> s { stInEndnote = True } - endnote <- inlinesToOpenXML opts ils - modify $ \s -> s { stInEndnote = False } - return endnote +inlineToOpenXML' opts (Span ("",["endnote"],[]) ils) = if isEnabled Ext_endnotes opts + then (do + modify $ \s -> s { stInEndnote = isEnabled Ext_endnotes opts } + endnote <- inlinesToOpenXML opts ils + modify $ \s -> s { stInEndnote = False } + return endnote) + else inlinesToOpenXML opts ils inlineToOpenXML' opts (Span ("",["csl-block"],[]) ils) = inlinesToOpenXML opts ils inlineToOpenXML' opts (Span ("",["csl-left-margin"],[]) ils) = @@ -847,11 +849,13 @@ inlineToOpenXML' opts (Span (ident,classes,kvs) ils) = do langmod $ inlinesToOpenXML opts ils wrapBookmark ident contents inlineToOpenXML' opts (Strong lst) = - withTextProp (mknode "w:bCs" [] ()) $ -- needed for LTR, #6911 + withTextProp (mknode "w:bCs" [] ()) $ -- needed for LTR, #6911 -- needed for LTR, #6911 + -- needed for LTR, #6911 withTextProp (mknode "w:b" [] ()) $ inlinesToOpenXML opts lst inlineToOpenXML' opts (Emph lst) = - withTextProp (mknode "w:iCs" [] ()) $ -- needed for LTR, #6911 + withTextProp (mknode "w:iCs" [] ()) $ -- needed for LTR, #6911 -- needed for LTR, #6911 + -- needed for LTR, #6911 withTextProp (mknode "w:i" [] ()) $ inlinesToOpenXML opts lst inlineToOpenXML' opts (Underline lst) = @@ -912,7 +916,7 @@ inlineToOpenXML' opts (Code attrs str) = do _ -> unhighlighted inlineToOpenXML' opts (Note bs) = do isEndnote <- gets stInEndnote - notes <- gets stFootnotes + notes <- gets $ if isEndnote then stEndnotes else stFootnotes notenum <- getUniqueId footnoteStyle <- rStyleM $ if isEndnote then "Endnote Reference" diff --git a/src/Text/Pandoc/Writers/Docx/Types.hs b/src/Text/Pandoc/Writers/Docx/Types.hs index a2a199e8c2b6..d985d98a16a0 100644 --- a/src/Text/Pandoc/Writers/Docx/Types.hs +++ b/src/Text/Pandoc/Writers/Docx/Types.hs @@ -185,8 +185,19 @@ defaultFootnotes = [ mknode "w:footnote" [ mknode "w:continuationSeparator" [] ()]]]] -- TODO: verify whether Word behaves the same with endnotes as it does with footnotes. +-- For now, let's do the same as for footnotes defaultEndnotes :: [Element] -defaultEndnotes = [] +-- defaultEndnotes = [] +defaultEndnotes = [ mknode "w:endnote" + [("w:type", "separator"), ("w:id", "-1")] + [ mknode "w:p" [] + [mknode "w:r" [] + [ mknode "w:separator" [] ()]]] + , mknode "w:endnote" + [("w:type", "continuationSeparator"), ("w:id", "0")] + [ mknode "w:p" [] + [ mknode "w:r" [] + [ mknode "w:continuationSeparator" [] ()]]]] pStyleM :: (PandocMonad m) => ParaStyleName -> WS m XML.Element pStyleM styleName = do From 932d82646e51e59ce327036c515dc7df612972db Mon Sep 17 00:00:00 2001 From: massifrg Date: Wed, 4 Mar 2026 19:04:16 +0100 Subject: [PATCH 04/12] DOCX writer: endnotes ext working The "endnotes" extension for docx is working with the docx Writer. No tests for this feature yet, but old tests now pass. --- src/Text/Pandoc/Writers/Docx.hs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs index 6f6a8f87e9a4..a7aff82325b8 100644 --- a/src/Text/Pandoc/Writers/Docx.hs +++ b/src/Text/Pandoc/Writers/Docx.hs @@ -106,6 +106,7 @@ writeDocx opts doc = do -- Phase 5: Relationship extraction (baserels, headers, footers, newMaxRelId) <- extractRelationships opts refArchive distArchive + (baserels, headers, footers, newMaxRelId) <- extractRelationships opts refArchive distArchive let initialSt = defaultWriterState { stStyleMaps = styleMaps @@ -138,6 +139,7 @@ writeDocx opts doc = do -- e.g. deleting the reference to footnotes.xml or removing default entries -- for image content types. let contentTypesEntry = mkContentTypesEntry epochtime imgs headers footers endnotes refArchive + let contentTypesEntry = mkContentTypesEntry epochtime imgs headers footers endnotes refArchive let relEntry = mkDocumentRelsEntry epochtime baserels imgs (stExternalLinks st) let contentEntry = toEntry "word/document.xml" epochtime (BL.fromStrict $ UTF8.fromText contents) @@ -173,6 +175,12 @@ writeDocx opts doc = do ++ if (isEnabled Ext_endnotes opts) then [endnoteRelEntry, endnotesEntry] else [] + commentsEntry : docPropsEntry : customPropsEntry : settingsEntry : + imageEntries + ++ refEntries + ++ if (isEnabled Ext_endnotes opts) + then [endnoteRelEntry, endnotesEntry] + else [] return $ fromArchive archive newParaPropToOpenXml :: ParaStyleName -> Element @@ -529,8 +537,10 @@ extractPageLayout refArchive distArchive = do -- | Parse and augment relationships from reference.docx extractRelationships :: PandocMonad m + => WriterOptions -> Archive -> Archive => WriterOptions -> Archive -> Archive -> m ([Element], [Element], [Element], Int) +extractRelationships opts refArchive distArchive = do extractRelationships opts refArchive distArchive = do let isImageNode e = findAttr (QName "Type" Nothing Nothing) e == Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" let isHeaderNode e = findAttr (QName "Type" Nothing Nothing) e == Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" @@ -558,6 +568,7 @@ extractRelationships opts refArchive distArchive = do ,("Target",target')] () : rels) _ -> (maxId, rels) + let (newMaxRelId, baserels) = foldr addBaseRel (maxRelId, parsedRels) $ let (newMaxRelId, baserels) = foldr addBaseRel (maxRelId, parsedRels) $ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", "numbering.xml") @@ -577,6 +588,8 @@ extractRelationships opts refArchive distArchive = do "comments.xml") ] ++ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", "endnotes.xml") | isEnabled Ext_endnotes opts] + ] ++ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", + "endnotes.xml") | isEnabled Ext_endnotes opts] return (baserels, headers, footers, newMaxRelId) @@ -653,8 +666,10 @@ mkContentTypesEntry :: Integer -> [Element] -- headers -> [Element] -- footers -> [Element] -- endnotes + -> [Element] -- endnotes -> Archive -- refArchive -> Entry +mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = let mkOverrideNode (part', contentType') = mknode "Override" [("PartName", T.pack part') @@ -665,8 +680,8 @@ mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = mkMediaOverride imgpath = mkOverrideNode ("/" <> imgpath, getMimeTypeDef imgpath) endnotesOverride = if null endnotes - then [("/word/endnotes.xml", - "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml")] + then map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), + "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml")) endnotes else [] overrides = map mkOverrideNode ( @@ -696,6 +711,7 @@ mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml") ] ++ endnotesOverride ++ + endnotesOverride ++ map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml")) headers ++ map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), @@ -755,6 +771,7 @@ mkStylesEntry epochtime styledoc styleMaps st opts = (\sty -> not $ hasStyleName sty $ smCharStyle styleMaps) (Set.toList $ stDynamicTextProps st) + -- TODO: add styles for endnotes, when Ext_endnotes is enabled -- TODO: add styles for endnotes, when Ext_endnotes is enabled newstyles = map newParaPropToOpenXml newDynamicParaProps ++ map newTextPropToOpenXml newDynamicTextProps ++ From fcaeb6ee5f68a7d3ba9caa7cc1a2858f10e383bb Mon Sep 17 00:00:00 2001 From: massifrg Date: Thu, 5 Mar 2026 11:12:36 +0100 Subject: [PATCH 05/12] DOCX writer: fix prev broken commit --- src/Text/Pandoc/Writers/Docx.hs | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs index a7aff82325b8..6f6a8f87e9a4 100644 --- a/src/Text/Pandoc/Writers/Docx.hs +++ b/src/Text/Pandoc/Writers/Docx.hs @@ -106,7 +106,6 @@ writeDocx opts doc = do -- Phase 5: Relationship extraction (baserels, headers, footers, newMaxRelId) <- extractRelationships opts refArchive distArchive - (baserels, headers, footers, newMaxRelId) <- extractRelationships opts refArchive distArchive let initialSt = defaultWriterState { stStyleMaps = styleMaps @@ -139,7 +138,6 @@ writeDocx opts doc = do -- e.g. deleting the reference to footnotes.xml or removing default entries -- for image content types. let contentTypesEntry = mkContentTypesEntry epochtime imgs headers footers endnotes refArchive - let contentTypesEntry = mkContentTypesEntry epochtime imgs headers footers endnotes refArchive let relEntry = mkDocumentRelsEntry epochtime baserels imgs (stExternalLinks st) let contentEntry = toEntry "word/document.xml" epochtime (BL.fromStrict $ UTF8.fromText contents) @@ -175,12 +173,6 @@ writeDocx opts doc = do ++ if (isEnabled Ext_endnotes opts) then [endnoteRelEntry, endnotesEntry] else [] - commentsEntry : docPropsEntry : customPropsEntry : settingsEntry : - imageEntries - ++ refEntries - ++ if (isEnabled Ext_endnotes opts) - then [endnoteRelEntry, endnotesEntry] - else [] return $ fromArchive archive newParaPropToOpenXml :: ParaStyleName -> Element @@ -537,10 +529,8 @@ extractPageLayout refArchive distArchive = do -- | Parse and augment relationships from reference.docx extractRelationships :: PandocMonad m - => WriterOptions -> Archive -> Archive => WriterOptions -> Archive -> Archive -> m ([Element], [Element], [Element], Int) -extractRelationships opts refArchive distArchive = do extractRelationships opts refArchive distArchive = do let isImageNode e = findAttr (QName "Type" Nothing Nothing) e == Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" let isHeaderNode e = findAttr (QName "Type" Nothing Nothing) e == Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" @@ -568,7 +558,6 @@ extractRelationships opts refArchive distArchive = do ,("Target",target')] () : rels) _ -> (maxId, rels) - let (newMaxRelId, baserels) = foldr addBaseRel (maxRelId, parsedRels) $ let (newMaxRelId, baserels) = foldr addBaseRel (maxRelId, parsedRels) $ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", "numbering.xml") @@ -588,8 +577,6 @@ extractRelationships opts refArchive distArchive = do "comments.xml") ] ++ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", "endnotes.xml") | isEnabled Ext_endnotes opts] - ] ++ [("http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", - "endnotes.xml") | isEnabled Ext_endnotes opts] return (baserels, headers, footers, newMaxRelId) @@ -666,10 +653,8 @@ mkContentTypesEntry :: Integer -> [Element] -- headers -> [Element] -- footers -> [Element] -- endnotes - -> [Element] -- endnotes -> Archive -- refArchive -> Entry -mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = let mkOverrideNode (part', contentType') = mknode "Override" [("PartName", T.pack part') @@ -680,8 +665,8 @@ mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = mkMediaOverride imgpath = mkOverrideNode ("/" <> imgpath, getMimeTypeDef imgpath) endnotesOverride = if null endnotes - then map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), - "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml")) endnotes + then [("/word/endnotes.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml")] else [] overrides = map mkOverrideNode ( @@ -711,7 +696,6 @@ mkContentTypesEntry epochtime imgs headers footers endnotes refArchive = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml") ] ++ endnotesOverride ++ - endnotesOverride ++ map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml")) headers ++ map (\x -> (maybe "" (T.unpack . ("/word/" <>)) (extractTarget x), @@ -771,7 +755,6 @@ mkStylesEntry epochtime styledoc styleMaps st opts = (\sty -> not $ hasStyleName sty $ smCharStyle styleMaps) (Set.toList $ stDynamicTextProps st) - -- TODO: add styles for endnotes, when Ext_endnotes is enabled -- TODO: add styles for endnotes, when Ext_endnotes is enabled newstyles = map newParaPropToOpenXml newDynamicParaProps ++ map newTextPropToOpenXml newDynamicTextProps ++ From b85eb017a13bbb43a62444e4ee7373664141255e Mon Sep 17 00:00:00 2001 From: massifrg Date: Thu, 5 Mar 2026 12:23:08 +0100 Subject: [PATCH 06/12] DOCX writer: endnotes may have id,attrs When "endnotes" extension is enabled, the Span of class "endnote" that embeds a Note may have an id or attributes for the Note to become an endnote. The only condition is that there is ONLY ONE class and that must be "endnote". --- src/Text/Pandoc/Writers/Docx/OpenXML.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Text/Pandoc/Writers/Docx/OpenXML.hs b/src/Text/Pandoc/Writers/Docx/OpenXML.hs index 63e5029da611..1fc28e7ea32b 100644 --- a/src/Text/Pandoc/Writers/Docx/OpenXML.hs +++ b/src/Text/Pandoc/Writers/Docx/OpenXML.hs @@ -756,7 +756,7 @@ inlineToOpenXML' opts SoftBreak = inlineToOpenXML opts (Str " ") inlineToOpenXML' opts (Span ("",["mark"],[]) ils) = withTextProp (mknode "w:highlight" [("w:val","yellow")] ()) $ inlinesToOpenXML opts ils -inlineToOpenXML' opts (Span ("",["endnote"],[]) ils) = if isEnabled Ext_endnotes opts +inlineToOpenXML' opts (Span (_,["endnote"],_) ils) = if isEnabled Ext_endnotes opts then (do modify $ \s -> s { stInEndnote = isEnabled Ext_endnotes opts } endnote <- inlinesToOpenXML opts ils From 3d3d4c19138bd1e3e1e5ed331bbc17268c960313 Mon Sep 17 00:00:00 2001 From: massifrg Date: Thu, 5 Mar 2026 12:24:54 +0100 Subject: [PATCH 07/12] DOCX reader: support for endnotes When the "endotes" extension is enabled, docx endnotes are converted to a Note embedded in a Span of class "endnote". --- src/Text/Pandoc/Readers/Docx.hs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Text/Pandoc/Readers/Docx.hs b/src/Text/Pandoc/Readers/Docx.hs index da0cca1dc506..152c9584eaf9 100644 --- a/src/Text/Pandoc/Readers/Docx.hs +++ b/src/Text/Pandoc/Readers/Docx.hs @@ -52,7 +52,9 @@ implemented, [-] means partially implemented): - [X] Link (links to an arbitrary bookmark create a span with the target as id and "anchor" class) - [X] Image - - [X] Note (Footnotes and Endnotes are silently combined.) + - [X] Note (Footnotes and Endnotes are silently combined, + unless Ext_endnotes is enabled: in that case a Note is embedded + in a Span with class "endnote") -} module Text.Pandoc.Readers.Docx @@ -342,7 +344,12 @@ runToInlines (Run rs runElems) transform <- runStyleToTransform rPr return $ transform ils runToInlines (Footnote bps) = note . smushBlocks <$> mapM bodyPartToBlocks bps -runToInlines (Endnote bps) = note . smushBlocks <$> mapM bodyPartToBlocks bps +runToInlines (Endnote bps) = do + isEndnotesExtEnabled <- asks (isEnabled Ext_endnotes . docxOptions) + noteInlines <- note . smushBlocks <$> mapM bodyPartToBlocks bps + return $ if isEndnotesExtEnabled + then spanWith ("", ["endnote"], []) noteInlines + else noteInlines runToInlines (InlineDrawing fp title alt bs ext) = do (lift . lift) $ P.insertMedia fp Nothing bs return $ imageWith (extentToAttr ext) (T.pack fp) title $ text alt From f29a0d6c469fe0b12390fb66baac26d8ede67f32 Mon Sep 17 00:00:00 2001 From: massifrg Date: Mon, 9 Mar 2026 09:36:15 +0100 Subject: [PATCH 08/12] MANUAL: endnotes support in docx --- MANUAL.txt | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/MANUAL.txt b/MANUAL.txt index 716ffb199a28..88fa81b641f0 100644 --- a/MANUAL.txt +++ b/MANUAL.txt @@ -5923,6 +5923,32 @@ 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. + +This extension is supported by the docx reader: when you convert +with `-f docx+endnotes`, the endnotes of the docx file will become +notes embedded in a span with class "endnote". + +It is also supported by the docx writer: when you convert with +`-t docx+endnotes`, all the notes embedded in a span of class "endnote" +will be endnotes in the docx file. + ## Citation syntax ### Extension: `citations` ### From 4325c01641e4f6aba05b6ae0603a73074b0304c4 Mon Sep 17 00:00:00 2001 From: massifrg Date: Mon, 9 Mar 2026 09:54:26 +0100 Subject: [PATCH 09/12] DOCX reader and writer: endnotes test --- test/command/11501.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 test/command/11501.md diff --git a/test/command/11501.md b/test/command/11501.md new file mode 100644 index 000000000000..7ac324c72651 --- /dev/null +++ b/test/command/11501.md @@ -0,0 +1,26 @@ +``` +% pandoc -f markdown -t docx+endnotes -o - | pandoc -f docx+endnotes -t markdown +First paragraph with an endnote[[^e1]]{.endnote} and a footnote[^1]. + +Second paragraph with a footnote[^2] and an endnote [[^e2]]{.endnote}. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^e1]: First endnote. + +[^e2]: Second endnote. +^D +First paragraph with an endnote[[^1]]{.endnote} and a footnote[^2]. + +Second paragraph with a footnote[^3] and an endnote [[^4]]{.endnote}. + +[^1]: First endnote. + +[^2]: First footnote. + +[^3]: Second footnote. + +[^4]: Second endnote. +``` \ No newline at end of file From fed5c53b4e9a6b277070b7e5f88a9db25d7335a9 Mon Sep 17 00:00:00 2001 From: massifrg Date: Mon, 9 Mar 2026 14:58:50 +0100 Subject: [PATCH 10/12] ODT writer: support for endnotes All the Notes inside a Span with class "endnote" are converted into endnotes in the resulting ODT file. --- src/Text/Pandoc/Extensions.hs | 1 + src/Text/Pandoc/Readers/ODT.hs | 14 +++++----- src/Text/Pandoc/Readers/ODT/ContentReader.hs | 15 ++++++++--- src/Text/Pandoc/Writers/OpenDocument.hs | 27 +++++++++++++++----- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/Text/Pandoc/Extensions.hs b/src/Text/Pandoc/Extensions.hs index 82dadb36ac3b..6ef19648d5c2 100644 --- a/src/Text/Pandoc/Extensions.hs +++ b/src/Text/Pandoc/Extensions.hs @@ -540,6 +540,7 @@ getAllExtensions f = universalExtensions <> getAll f ] getAll "opendocument" = extensionsFromList [ Ext_empty_paragraphs + , Ext_endnotes , Ext_native_numbering , Ext_xrefs_name , Ext_xrefs_number diff --git a/src/Text/Pandoc/Readers/ODT.hs b/src/Text/Pandoc/Readers/ODT.hs index e4c2900ff5be..3b94b112e469 100644 --- a/src/Text/Pandoc/Readers/ODT.hs +++ b/src/Text/Pandoc/Readers/ODT.hs @@ -70,20 +70,20 @@ makeFigure x = x readODT' :: ReaderOptions -> B.ByteString -> Either PandocError (Pandoc, MediaBag) -readODT' _ bytes = bytesToODT bytes-- of +readODT' opts bytes = bytesToODT opts bytes-- of -- Right (pandoc, mediaBag) -> Right (pandoc , mediaBag) -- Left err -> Left err -- -bytesToODT :: B.ByteString -> Either PandocError (Pandoc, MediaBag) -bytesToODT bytes = case toArchiveOrFail bytes of - Right archive -> archiveToODT archive +bytesToODT :: ReaderOptions -> B.ByteString -> Either PandocError (Pandoc, MediaBag) +bytesToODT opts bytes = case toArchiveOrFail bytes of + Right archive -> archiveToODT opts archive Left err -> Left $ PandocParseError $ "Could not unzip ODT: " <> T.pack err -- -archiveToODT :: Archive -> Either PandocError (Pandoc, MediaBag) -archiveToODT archive = do +archiveToODT :: ReaderOptions -> Archive -> Either PandocError (Pandoc, MediaBag) +archiveToODT opts archive = do let onFailure msg Nothing = Left $ PandocParseError msg onFailure _ (Just x) = Right x contentEntry <- onFailure "Could not find content.xml" @@ -101,7 +101,7 @@ archiveToODT archive = do let (dir, name) = splitFileName fp in (dir == "Pictures/") || (dir /= "./" && name == "content.xml") let media = filteredFilesFromArchive archive filePathIsODTMedia - let startState = readerState styles media + let startState = readerState opts styles media either (\_ -> Left $ PandocParseError "Could not convert opendocument") Right (runConverter' read_body startState contentElem) diff --git a/src/Text/Pandoc/Readers/ODT/ContentReader.hs b/src/Text/Pandoc/Readers/ODT/ContentReader.hs index 2e192f3e3230..121c44e1ba28 100644 --- a/src/Text/Pandoc/Readers/ODT/ContentReader.hs +++ b/src/Text/Pandoc/Readers/ODT/ContentReader.hs @@ -44,6 +44,7 @@ import Text.Pandoc.Builder hiding (underline) import Text.Pandoc.MediaBag (MediaBag, insertMedia) import Text.Pandoc.Shared import Text.Pandoc.Extensions (extensionsFromList, Extension(..)) +import Text.Pandoc.Options (isEnabled, ReaderOptions) import qualified Text.Pandoc.UTF8 as UTF8 import Text.Pandoc.Readers.Docx.Combine (combineBlocks) @@ -96,11 +97,13 @@ data ReaderState , envMedia :: Media -- | Hold binary resources used in the document , odtMediaBag :: MediaBag + -- | Read endnotes as Note inside a Span of class "endnote" + , readEndnotes :: Bool } deriving ( Show ) -readerState :: Styles -> Media -> ReaderState -readerState styles media = ReaderState styles [] 0 M.empty Nothing M.empty media mempty +readerState :: ReaderOptions -> Styles -> Media -> ReaderState +readerState opts styles media = ReaderState styles [] 0 M.empty Nothing M.empty media mempty (isEnabled Ext_endnotes opts) -- pushStyle' :: Style -> ReaderState -> ReaderState @@ -788,13 +791,17 @@ fixRelativeLink uri = _ -> uri ------------------------- --- Footnotes +-- Footnotes and Endnotes ------------------------- +endnote :: Blocks -> Inlines +endnote blocks = spanWith ("", ["endnote"], []) $ note blocks + read_note :: InlineMatcher read_note = matchingElement NsText "note" - $ liftA note + $ liftA note -- use endnote if readEndnotes is True and the text:note-class attribute is "endnote" $ matchChildContent' [ read_note_body ] + read_note_body :: BlockMatcher read_note_body = matchingElement NsText "note-body" diff --git a/src/Text/Pandoc/Writers/OpenDocument.hs b/src/Text/Pandoc/Writers/OpenDocument.hs index 0d49d72e10a4..79feab6e8cdf 100644 --- a/src/Text/Pandoc/Writers/OpenDocument.hs +++ b/src/Text/Pandoc/Writers/OpenDocument.hs @@ -64,6 +64,7 @@ data ReferenceType data WriterState = WriterState { stNotes :: [Doc Text] + , stEndnotes :: [Doc Text] , stTableStyles :: [Doc Text] , stParaStyles :: [Doc Text] , stListStyles :: [(Int, [Doc Text])] @@ -72,6 +73,7 @@ data WriterState = , stTextStyleAttr :: Set.Set TextStyle , stIndentPara :: Int , stInDefinition :: Bool + , stInEndnote :: Bool , stTight :: Bool , stFirstPara :: Bool , stImageId :: Int @@ -83,6 +85,7 @@ data WriterState = defaultWriterState :: WriterState defaultWriterState = WriterState { stNotes = [] + , stEndnotes = [] , stTableStyles = [] , stParaStyles = [] , stListStyles = [] @@ -90,6 +93,7 @@ defaultWriterState = , stTextStyleAttr = Set.empty , stIndentPara = 0 , stInDefinition = False + , stInEndnote = False , stTight = False , stFirstPara = False , stImageId = 1 @@ -636,6 +640,11 @@ inlineToOpenDocument o ils Span ("", ["mark"], []) xs -> inTags False "text:span" [("text:style-name","Highlighted")] <$> inlinesToOpenDocument o xs + Span (_, ["endnote"], _) xs | isEnabled Ext_endnotes o -> do + modify (\st -> st{ stInEndnote = True }) + s <- inlinesToOpenDocument o xs + modify (\st -> st{ stInEndnote = False }) + return s Span attr xs -> mkSpan attr xs LineBreak -> return $ selfClosingTag "text:line-break" [] Str s -> return $ handleSpaces $ escapeStringForXML s @@ -701,15 +710,21 @@ inlineToOpenDocument o ils if T.null ident then i else fmap mkBookmarkedSpan i - mkNote l = do - n <- length <$> gets stNotes - let footNote t = inTags False "text:note" - [ ("text:id" , "ftn" <> tshow n) - , ("text:note-class", "footnote" )] $ + mkNote l = do + inEndnote <- gets stInEndnote + nFootnote <- length <$> gets stNotes + nEndnote <- length <$> gets stEndnotes + let n = if inEndnote then nEndnote else nFootnote + idn = nFootnote + nEndnote + noteClass = if inEndnote then "endnote" else "footnote" + pStyleName = if inEndnote then "Endnote" else "Footnote" + footNote t = inTags False "text:note" + [ ("text:id" , "ftn" <> tshow idn) + , ("text:note-class", noteClass )] $ inTagsSimple "text:note-citation" (text . show $ n + 1) <> inTagsSimple "text:note-body" t nn <- footNote <$> withAlteredTextStyles (const mempty) - (withParagraphStyle o "Footnote" l) + (withParagraphStyle o pStyleName l) addNote nn return nn From 2d5d4c131d5040ee87355abb7feb35a5db82a5f9 Mon Sep 17 00:00:00 2001 From: massifrg Date: Mon, 9 Mar 2026 23:16:11 +0100 Subject: [PATCH 11/12] ODT reader: support for endnotes When the endnotes extension is enabled, the endnotes of the ODT file will be converted into Notes inside a Span with the class "endnote". --- MANUAL.txt | 11 ++++++----- src/Text/Pandoc/Readers/ODT/ContentReader.hs | 10 ++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/MANUAL.txt b/MANUAL.txt index 88fa81b641f0..537361929ae5 100644 --- a/MANUAL.txt +++ b/MANUAL.txt @@ -5941,13 +5941,14 @@ around them. Enabling this extension tells some readers and writers to use that convention to distinguish endnotes from footnotes. -This extension is supported by the docx reader: when you convert -with `-f docx+endnotes`, the endnotes of the docx file will become +This extension is supported by the docx and odt readers: +when you convert with `-f docx+endnotes` or `-f odt+endnotes`, +the endnotes of the docx or odt file will become notes embedded in a span with class "endnote". -It is also supported by the docx writer: when you convert with -`-t docx+endnotes`, all the notes embedded in a span of class "endnote" -will be endnotes in the docx file. +It is also supported by the docx and odt writers: when you convert +with `-t docx+endnotes` or `-t odt+endnotes`, all the notes embedded +in a span of class "endnote" will be endnotes in the docx or odt file. ## Citation syntax diff --git a/src/Text/Pandoc/Readers/ODT/ContentReader.hs b/src/Text/Pandoc/Readers/ODT/ContentReader.hs index 121c44e1ba28..5c951fa1f2fe 100644 --- a/src/Text/Pandoc/Readers/ODT/ContentReader.hs +++ b/src/Text/Pandoc/Readers/ODT/ContentReader.hs @@ -797,11 +797,17 @@ fixRelativeLink uri = endnote :: Blocks -> Inlines endnote blocks = spanWith ("", ["endnote"], []) $ note blocks +whichnote :: Bool -> T.Text -> (Blocks -> Inlines) +whichnote True "endnote" = endnote +whichnote _ _ = note + read_note :: InlineMatcher read_note = matchingElement NsText "note" - $ liftA note -- use endnote if readEndnotes is True and the text:note-class attribute is "endnote" + $ liftA3 whichnote readingEndnotes noteClass $ matchChildContent' [ read_note_body ] - + where + noteClass = findAttrTextWithDefault NsText "note-class" "" + readingEndnotes = getExtraState >>^ readEndnotes read_note_body :: BlockMatcher read_note_body = matchingElement NsText "note-body" From f2078f241b6b3106a3fbd7d7858415153d1f5b03 Mon Sep 17 00:00:00 2001 From: massifrg Date: Mon, 9 Mar 2026 23:57:16 +0100 Subject: [PATCH 12/12] ODT: test for endnotes support --- test/command/11516.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test/command/11516.md diff --git a/test/command/11516.md b/test/command/11516.md new file mode 100644 index 000000000000..844d15fc12ed --- /dev/null +++ b/test/command/11516.md @@ -0,0 +1,32 @@ +``` +% pandoc -f markdown -t odt+endnotes -o - | pandoc -f odt+endnotes -t markdown +First paragraph with an endnote[[^e1]]{.endnote} and a footnote[^1]. + +Second paragraph with a footnote[^2] and an endnote [[^e2]]{.endnote}. + +[^1]: First footnote. + +[^2]: Second footnote. + +[^e1]: First endnote. + +[^e2]: Second endnote. +^D +First paragraph with an endnote[[^1]]{.endnote} and a footnote[^2]. + +Second paragraph with a footnote[^3] and an endnote [[^4]]{.endnote}. + +[^1]: First endnote. + +[^2]: First footnote. + +[^3]: Second footnote. + +[^4]: Second endnote. +``` +``` +% pandoc -f markdown -t odt+endnotes -o - | pandoc -f odt+endnotes -t markdown --wrap=none +A paragraph with a [span]{.endnote} with an endnote class. +^D +A paragraph with a span with an endnote class. +```