From 418bda81282c82325c5a296a3c486fdc5ab1dfe0 Mon Sep 17 00:00:00 2001 From: John MacFarlane Date: Sat, 12 Aug 2017 22:42:51 -0700 Subject: [PATCH] Docx writer: pass through comments. We assume that comments are defined as parsed by the docx reader: I want I left a comment.some text to have a comment on it. We assume also that the id attributes are unique and properly matched between comment-start and comment-end. Closes #2994. --- pandoc.cabal | 1 + src/Text/Pandoc/Writers/Docx.hs | 55 +++++++++++++++++++++++++++----- test/command/2994.docx | Bin 0 -> 9701 bytes test/command/2994.md | 7 ++++ 4 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 test/command/2994.docx create mode 100644 test/command/2994.md diff --git a/pandoc.cabal b/pandoc.cabal index 9392b4430..39a390dd6 100644 --- a/pandoc.cabal +++ b/pandoc.cabal @@ -138,6 +138,7 @@ Extra-Source-Files: test/*.native test/command/*.md test/command/3533-rst-csv-tables.csv + test/command/2994.docx test/command/abbrevs test/command/SVG_logo-without-xml-declaration.svg test/command/SVG_logo.svg diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs index 8b19f3740..51e4ffb98 100644 --- a/src/Text/Pandoc/Writers/Docx.hs +++ b/src/Text/Pandoc/Writers/Docx.hs @@ -123,6 +123,7 @@ defaultWriterEnv = WriterEnv{ envTextProperties = [] data WriterState = WriterState{ stFootnotes :: [Element] + , stComments :: [([(String,String)], [Inline])] , stSectionIds :: Set.Set String , stExternalLinks :: M.Map String String , stImages :: M.Map FilePath (String, String, Maybe MimeType, Element, B.ByteString) @@ -139,6 +140,7 @@ data WriterState = WriterState{ defaultWriterState :: WriterState defaultWriterState = WriterState{ stFootnotes = defaultFootnotes + , stComments = [] , stSectionIds = Set.empty , stExternalLinks = M.empty , stImages = M.empty @@ -305,11 +307,11 @@ writeDocx opts doc@(Pandoc meta _) = do } - ((contents, footnotes), st) <- runStateT - (runReaderT - (writeOpenXML opts{writerWrapText = WrapNone} doc') - env) - initialSt + ((contents, footnotes, comments), st) <- runStateT + (runReaderT + (writeOpenXML opts{writerWrapText = WrapNone} doc') + env) + initialSt let epochtime = floor $ utcTimeToPOSIXSeconds utctime let imgs = M.elems $ stImages st @@ -370,6 +372,8 @@ writeDocx opts doc@(Pandoc meta _) = do "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml") ,("/word/document.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml") + ,("/word/comments.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml") ,("/word/footnotes.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml") ] ++ @@ -416,6 +420,9 @@ writeDocx opts doc@(Pandoc meta _) = do ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", "rId7", "footnotes.xml") + ,("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", + "rId8", + "comments.xml") ] let idMap = renumIdMap (length baserels' + 1) (headers ++ footers) @@ -461,6 +468,10 @@ writeDocx opts doc@(Pandoc meta _) = do $ renderXml $ mknode "Relationships" [("xmlns","http://schemas.openxmlformats.org/package/2006/relationships")] linkrels + -- comments + let commentsEntry = toEntry "word/comments.xml" epochtime + $ renderXml $ mknode "w:comments" stdAttributes comments + -- styles -- We only want to inject paragraph and text properties that @@ -564,6 +575,7 @@ writeDocx opts doc@(Pandoc meta _) = do let archive = foldr addEntryToArchive emptyArchive $ contentTypesEntry : relsEntry : contentEntry : relEntry : footnoteRelEntry : numEntry : styleEntry : footnotesEntry : + commentsEntry : docPropsEntry : docPropsAppEntry : themeEntry : fontTableEntry : settingsEntry : webSettingsEntry : imageEntries ++ headerFooterEntries ++ @@ -762,7 +774,7 @@ makeTOC _ = return [] -- | Convert Pandoc document to two lists of -- OpenXML elements (the main document and footnotes). -writeOpenXML :: (PandocMonad m) => WriterOptions -> Pandoc -> WS m ([Element], [Element]) +writeOpenXML :: (PandocMonad m) => WriterOptions -> Pandoc -> WS m ([Element], [Element],[Element]) writeOpenXML opts (Pandoc meta blocks) = do let tit = docTitle meta ++ case lookupMeta "subtitle" meta of Just (MetaBlocks [Plain xs]) -> LineBreak : xs @@ -791,10 +803,27 @@ writeOpenXML opts (Pandoc meta blocks) = do convertSpace xs = xs let blocks' = bottomUp convertSpace blocks doc' <- (setFirstPara >> blocksToOpenXML opts blocks') - notes' <- reverse `fmap` gets stFootnotes + notes' <- reverse <$> gets stFootnotes + comments <- reverse <$> gets stComments + let toComment (kvs, ils) = do + annotation <- inlinesToOpenXML opts ils + return $ + mknode "w:comment" [('w':':':k,v) | (k,v) <- kvs] + [ mknode "w:p" [] $ + [ mknode "w:pPr" [] + [ mknode "w:pStyle" [("w:val", "CommentText")] () ] + , mknode "w:r" [] + [ mknode "w:rPr" [] + [ mknode "w:rStyle" [("w:val", "CommentReference")] () + , mknode "w:annotationRef" [] () + ] + ] + ] ++ annotation + ] + comments' <- mapM toComment comments toc <- makeTOC opts let meta' = title ++ subtitle ++ authors ++ date ++ abstract ++ toc - return (meta' ++ doc', notes') + return (meta' ++ doc', notes', comments') -- | Convert a list of Pandoc blocks to OpenXML. blocksToOpenXML :: (PandocMonad m) => WriterOptions -> [Block] -> WS m [Element] @@ -1101,6 +1130,16 @@ inlineToOpenXML' _ (Str str) = formattedString str inlineToOpenXML' opts Space = inlineToOpenXML opts (Str " ") inlineToOpenXML' opts SoftBreak = inlineToOpenXML opts (Str " ") +inlineToOpenXML' _ (Span (ident,["comment-start"],kvs) ils) = do + modify $ \st -> st{ stComments = (("id",ident):kvs, ils) : stComments st } + return [ mknode "w:commentRangeStart" [("w:id", ident)] () ] +inlineToOpenXML' _ (Span (ident,["comment-end"],_) _) = do + return [ mknode "w:commentRangeEnd" [("w:id", ident)] () + , mknode "w:r" [] + [ mknode "w:rPr" [] + [ mknode "w:rStyle" [("w:val", "CommentReference")] () ] + , mknode "w:commentReference" [("w:id", ident)] () ] + ] inlineToOpenXML' opts (Span (ident,classes,kvs) ils) = do stylemod <- case lookup dynamicStyleKey kvs of Just sty -> do diff --git a/test/command/2994.docx b/test/command/2994.docx new file mode 100644 index 0000000000000000000000000000000000000000..44118e9bd9e8d8723c689b266bb1d1b4039bc481 GIT binary patch literal 9701 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