git.iko.soy/typeable/pandoc
on master help sign in
Merge pull request #1968 from lierdakil/issue1607

Fixes for multiple docx writer style bugs.

commit619b2e8ca207b39829b74683cfc514ffbf4ddbbb authorJohn MacFarlane <jgm@berkeley.edu> dateMon 16 Mar 2015, 12:02 (12 years ago) parent6fc578b59c4d28
16 files changed+401−100
data/reference.docx pandoc.cabal+3−1 src/Text/Pandoc/Readers/Docx/Parse.hs+6−20 src/Text/Pandoc/Readers/Docx/StyleMap.hs+105 src/Text/Pandoc/Readers/Docx/Util.hs+26 src/Text/Pandoc/Writers/Docx.hs+99−79 tests/Tests/Writers/Docx.hs+128 tests/docx/image_no_embed_writer.native+2 tests/docx/inline_formatting_writer.native+5 tests/docx/inline_images_writer.native+2 tests/docx/links_writer.native+6 tests/docx/lists_writer.native+17 tests/media/rId25.jpg tests/media/rId26.jpg tests/media/rId27.jpg tests/test-pandoc.hs+2
data/reference.docx

Binary file not shown.

pandoc.cabal+3−1
@@ -338,7 +338,9 @@ Library
338338 Other-Modules: Text.Pandoc.Readers.Docx.Lists,
339339 Text.Pandoc.Readers.Docx.Reducible,
340340 Text.Pandoc.Readers.Docx.Parse,
341− Text.Pandoc.Readers.Docx.Fonts
341+ Text.Pandoc.Readers.Docx.Fonts,
342+ Text.Pandoc.Readers.Docx.Util,
343+ Text.Pandoc.Readers.Docx.StyleMap
342344 Text.Pandoc.Writers.Shared,
343345 Text.Pandoc.Asciify,
344346 Text.Pandoc.MIME,
src/Text/Pandoc/Readers/Docx/Parse.hs+6−20
@@ -65,6 +65,7 @@ import Text.Pandoc.Compat.Except
6565import Text.TeXMath.Readers.OMML (readOMML)
6666import Text.Pandoc.Readers.Docx.Fonts (getUnicode, Font(..))
6767import Text.TeXMath (Exp)
68+import Text.Pandoc.Readers.Docx.Util
6869import Data.Char (readLitChar, ord, chr, isDigit)
6970
7071data ReaderEnv = ReaderEnv { envNotes :: Notes
@@ -108,8 +109,6 @@ mapD f xs =
108109 in
109110 concatMapM handler xs
110111
111−type NameSpaces = [(String, String)]
112−
113112data Docx = Docx Document
114113 deriving Show
115114
@@ -249,10 +248,6 @@ type ChangeId = String
249248type Author = String
250249type ChangeDate = String
251250
252−attrToNSPair :: Attr -> Maybe (String, String)
253−attrToNSPair (Attr (QName s _ (Just "xmlns")) val) = Just (s, val)
254−attrToNSPair _ = Nothing
255−
256251archiveToDocx :: Archive -> Either DocxError Docx
257252archiveToDocx archive = do
258253 let notes = archiveToNotes archive
@@ -269,7 +264,7 @@ archiveToDocument :: Archive -> D Document
269264archiveToDocument zf = do
270265 entry <- maybeToD $ findEntryByPath "word/document.xml" zf
271266 docElem <- maybeToD $ (parseXMLDoc . UTF8.toStringLazy . fromEntry) entry
272− let namespaces = mapMaybe attrToNSPair (elAttribs docElem)
267+ let namespaces = elemToNameSpaces docElem
273268 bodyElem <- maybeToD $ findChild (elemName namespaces "w" "body") docElem
274269 body <- elemToBody namespaces bodyElem
275270 return $ Document namespaces body
@@ -288,7 +283,7 @@ archiveToStyles zf =
288283 case stylesElem of
289284 Nothing -> (M.empty, M.empty)
290285 Just styElem ->
291− let namespaces = mapMaybe attrToNSPair (elAttribs styElem)
286+ let namespaces = elemToNameSpaces styElem
292287 in
293288 ( M.fromList $ buildBasedOnList namespaces styElem
294289 (Nothing :: Maybe CharStyle),
@@ -356,10 +351,10 @@ archiveToNotes zf =
356351 enElem = findEntryByPath "word/endnotes.xml" zf
357352 >>= (parseXMLDoc . UTF8.toStringLazy . fromEntry)
358353 fn_namespaces = case fnElem of
359− Just e -> mapMaybe attrToNSPair (elAttribs e)
354+ Just e -> elemToNameSpaces e
360355 Nothing -> []
361356 en_namespaces = case enElem of
362− Just e -> mapMaybe attrToNSPair (elAttribs e)
357+ Just e -> elemToNameSpaces e
363358 Nothing -> []
364359 ns = unionBy (\x y -> fst x == fst y) fn_namespaces en_namespaces
365360 fn = fnElem >>= (elemToNotes ns "footnote")
@@ -459,7 +454,7 @@ archiveToNumbering' zf = do
459454 Nothing -> Just $ Numbering [] [] []
460455 Just entry -> do
461456 numberingElem <- (parseXMLDoc . UTF8.toStringLazy . fromEntry) entry
462− let namespaces = mapMaybe attrToNSPair (elAttribs numberingElem)
457+ let namespaces = elemToNameSpaces numberingElem
463458 numElems = findChildren
464459 (QName "num" (lookup "w" namespaces) (Just "w"))
465460 numberingElem
@@ -488,15 +483,6 @@ elemToNotes _ _ _ = Nothing
488483---------------------------------------------
489484---------------------------------------------
490485
491−elemName :: NameSpaces -> String -> String -> QName
492−elemName ns prefix name = (QName name (lookup prefix ns) (Just prefix))
493−
494−isElem :: NameSpaces -> String -> String -> Element -> Bool
495−isElem ns prefix name element =
496− qName (elName element) == name &&
497− qURI (elName element) == (lookup prefix ns)
498−
499−
500486elemToTblGrid :: NameSpaces -> Element -> D TblGrid
501487elemToTblGrid ns element | isElem ns "w" "tblGrid" element =
502488 let cols = findChildren (elemName ns "w" "gridCol") element
src/Text/Pandoc/Readers/Docx/StyleMap.hsnew file+105
@@ -0,0 +1,105 @@
1+module Text.Pandoc.Readers.Docx.StyleMap ( StyleMaps(..)
2+ , defaultStyleMaps
3+ , getStyleMaps
4+ , getStyleId
5+ , hasStyleName
6+ ) where
7+
8+import Text.XML.Light
9+import Text.Pandoc.Readers.Docx.Util
10+import Control.Monad.State
11+import Data.Char (toLower)
12+import Data.Maybe (fromMaybe)
13+import qualified Data.Map as M
14+
15+newtype ParaStyleMap = ParaStyleMap ( M.Map String String )
16+newtype CharStyleMap = CharStyleMap ( M.Map String String )
17+
18+class StyleMap a where
19+ alterMap :: (M.Map String String -> M.Map String String) -> a -> a
20+ getMap :: a -> M.Map String String
21+
22+instance StyleMap ParaStyleMap where
23+ alterMap f (ParaStyleMap m) = ParaStyleMap $ f m
24+ getMap (ParaStyleMap m) = m
25+
26+instance StyleMap CharStyleMap where
27+ alterMap f (CharStyleMap m) = CharStyleMap $ f m
28+ getMap (CharStyleMap m) = m
29+
30+insert :: (StyleMap a) => String -> String -> a -> a
31+insert k v = alterMap $ M.insert k v
32+
33+getStyleId :: (StyleMap a) => String -> a -> String
34+getStyleId s = M.findWithDefault (filter (/=' ') s) (map toLower s) . getMap
35+
36+hasStyleName :: (StyleMap a) => String -> a -> Bool
37+hasStyleName styleName = M.member (map toLower styleName) . getMap
38+
39+data StyleMaps = StyleMaps { sNameSpaces :: NameSpaces
40+ , sParaStyleMap :: ParaStyleMap
41+ , sCharStyleMap :: CharStyleMap
42+ }
43+
44+data StyleType = ParaStyle | CharStyle
45+
46+defaultStyleMaps :: StyleMaps
47+defaultStyleMaps = StyleMaps { sNameSpaces = []
48+ , sParaStyleMap = ParaStyleMap M.empty
49+ , sCharStyleMap = CharStyleMap M.empty
50+ }
51+
52+type StateM a = StateT StyleMaps Maybe a
53+
54+getStyleMaps :: Element -> StyleMaps
55+getStyleMaps docElem = fromMaybe state' $ execStateT genStyleMap state'
56+ where
57+ state' = defaultStyleMaps {sNameSpaces = elemToNameSpaces docElem}
58+ genStyleItem e = do
59+ styleType <- getStyleType e
60+ styleId <- getAttrStyleId e
61+ nameValLowercase <- map toLower `fmap` getNameVal e
62+ case styleType of
63+ ParaStyle -> modParaStyleMap $ insert nameValLowercase styleId
64+ CharStyle -> modCharStyleMap $ insert nameValLowercase styleId
65+ genStyleMap = do
66+ style <- elemName' "style"
67+ let styles = findChildren style docElem
68+ forM_ styles genStyleItem
69+
70+modParaStyleMap :: (ParaStyleMap -> ParaStyleMap) -> StateM ()
71+modParaStyleMap f = modify $ \s ->
72+ s {sParaStyleMap = f $ sParaStyleMap s}
73+
74+modCharStyleMap :: (CharStyleMap -> CharStyleMap) -> StateM ()
75+modCharStyleMap f = modify $ \s ->
76+ s {sCharStyleMap = f $ sCharStyleMap s}
77+
78+getStyleType :: Element -> StateM StyleType
79+getStyleType e = do
80+ styleTypeStr <- getAttrType e
81+ case styleTypeStr of
82+ "paragraph" -> return ParaStyle
83+ "character" -> return CharStyle
84+ _ -> lift Nothing
85+
86+getAttrType :: Element -> StateM String
87+getAttrType el = do
88+ name <- elemName' "type"
89+ lift $ findAttr name el
90+
91+getAttrStyleId :: Element -> StateM String
92+getAttrStyleId el = do
93+ name <- elemName' "styleId"
94+ lift $ findAttr name el
95+
96+getNameVal :: Element -> StateM String
97+getNameVal el = do
98+ name <- elemName' "name"
99+ val <- elemName' "val"
100+ lift $ findChild name el >>= findAttr val
101+
102+elemName' :: String -> StateM QName
103+elemName' name = do
104+ namespaces <- gets sNameSpaces
105+ return $ elemName namespaces "w" name
src/Text/Pandoc/Readers/Docx/Util.hsnew file+26
@@ -0,0 +1,26 @@
1+module Text.Pandoc.Readers.Docx.Util (
2+ NameSpaces
3+ , elemName
4+ , isElem
5+ , elemToNameSpaces
6+ ) where
7+
8+import Text.XML.Light
9+import Data.Maybe (mapMaybe)
10+
11+type NameSpaces = [(String, String)]
12+
13+elemToNameSpaces :: Element -> NameSpaces
14+elemToNameSpaces = mapMaybe attrToNSPair . elAttribs
15+
16+attrToNSPair :: Attr -> Maybe (String, String)
17+attrToNSPair (Attr (QName s _ (Just "xmlns")) val) = Just (s, val)
18+attrToNSPair _ = Nothing
19+
20+elemName :: NameSpaces -> String -> String -> QName
21+elemName ns prefix name = QName name (lookup prefix ns) (Just prefix)
22+
23+isElem :: NameSpaces -> String -> String -> Element -> Bool
24+isElem ns prefix name element =
25+ qName (elName element) == name &&
26+ qURI (elName element) == lookup prefix ns
src/Text/Pandoc/Writers/Docx.hs+99−79
@@ -29,7 +29,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2929Conversion of 'Pandoc' documents to docx.
3030-}
3131module Text.Pandoc.Writers.Docx ( writeDocx ) where
32−import Data.List ( intercalate, isPrefixOf, isSuffixOf, stripPrefix )
32+import Data.List ( intercalate, isPrefixOf, isSuffixOf )
3333import qualified Data.ByteString as B
3434import qualified Data.ByteString.Lazy as BL
3535import qualified Data.ByteString.Lazy.Char8 as BL8
@@ -54,6 +54,8 @@ import Text.Pandoc.Walk
5454import Text.Highlighting.Kate.Types ()
5555import Text.XML.Light as XML
5656import Text.TeXMath
57+import Text.Pandoc.Readers.Docx.StyleMap
58+import Text.Pandoc.Readers.Docx.Util (elemName)
5759import Control.Monad.State
5860import Text.Highlighting.Kate
5961import Data.Unique (hashUnique, newUnique)
@@ -63,8 +65,7 @@ import qualified Control.Exception as E
6365import Text.Pandoc.MIME (MimeType, getMimeType, getMimeTypeDef,
6466 extensionFromMimeType)
6567import Control.Applicative ((<$>), (<|>), (<*>))
66−import Data.Maybe (fromMaybe, mapMaybe)
67−import Data.Char (isDigit)
68+import Data.Maybe (fromMaybe, mapMaybe, maybeToList)
6869
6970data ListMarker = NoMarker
7071 | BulletMarker
@@ -106,7 +107,7 @@ data WriterState = WriterState{
106107 , stChangesAuthor :: String
107108 , stChangesDate :: String
108109 , stPrintWidth :: Integer
109− , stHeadingStyles :: [(Int,String)]
110+ , stStyleMaps :: StyleMaps
110111 , stFirstPara :: Bool
111112 }
112113
@@ -127,7 +128,7 @@ defaultWriterState = WriterState{
127128 , stChangesAuthor = "unknown"
128129 , stChangesDate = "1969-12-31T19:00:00Z"
129130 , stPrintWidth = 1
130− , stHeadingStyles = []
131+ , stStyleMaps = defaultStyleMaps
131132 , stFirstPara = False
132133 }
133134
@@ -215,32 +216,14 @@ writeDocx opts doc@(Pandoc meta _) = do
215216 styledoc <- parseXml refArchive distArchive stylepath
216217
217218 -- parse styledoc for heading styles
218− let styleNamespaces = map ((,) <$> qName . attrKey <*> attrVal) .
219− filter ((==Just "xmlns") . qPrefix . attrKey) .
220− elAttribs $ styledoc
221− let headingStyles =
222− let
223− mywURI = lookup "w" styleNamespaces
224− myName name = QName name mywURI (Just "w")
225− getAttrStyleId = findAttr (myName "styleId")
226− getNameVal = findChild (myName "name") >=> findAttr (myName "val")
227− getNum s | not $ null s, all isDigit s = Just (read s :: Int)
228− | otherwise = Nothing
229− getEngHeader = getAttrStyleId >=> stripPrefix "Heading" >=> getNum
230− getIntHeader = getNameVal >=> stripPrefix "heading " >=> getNum
231− toTuple getF = liftM2 (,) <$> getF <*> getAttrStyleId
232− toMap getF = mapMaybe (toTuple getF) $
233− findChildren (myName "style") styledoc
234− select a b | not $ null a = a
235− | otherwise = b
236− in
237− select (toMap getEngHeader) (toMap getIntHeader)
219+ let styleMaps = getStyleMaps styledoc
238220
239221 ((contents, footnotes), st) <- runStateT (writeOpenXML opts{writerWrapText = False} doc')
240222 defaultWriterState{ stChangesAuthor = fromMaybe "unknown" username
241223 , stChangesDate = formatTime defaultTimeLocale "%FT%XZ" utctime
242224 , stPrintWidth = (maybe 420 (\x -> quot x 20) pgContentWidth)
243− , stHeadingStyles = headingStyles}
225+ , stStyleMaps = styleMaps
226+ }
244227 let epochtime = floor $ utcTimeToPOSIXSeconds utctime
245228 let imgs = M.elems $ stImages st
246229
@@ -393,9 +376,18 @@ writeDocx opts doc@(Pandoc meta _) = do
393376 linkrels
394377
395378 -- styles
396− let newstyles = styleToOpenXml $ writerHighlightStyle opts
397− let styledoc' = styledoc{ elContent = elContent styledoc ++
398− [Elem x | x <- newstyles, writerHighlight opts] }
379+ let newstyles = styleToOpenXml styleMaps $ writerHighlightStyle opts
380+ let styledoc' = styledoc{ elContent = modifyContent (elContent styledoc) }
381+ where
382+ modifyContent
383+ | writerHighlight opts = (++ map Elem newstyles)
384+ | otherwise = filter notTokStyle
385+ notTokStyle (Elem el) = notStyle el || notTokId el
386+ notTokStyle _ = True
387+ notStyle = (/= elemName' "style") . elName
388+ notTokId = maybe True (`notElem` tokStys) . findAttr (elemName' "styleId")
389+ tokStys = "SourceCode" : map show (enumFromTo KeywordTok NormalTok)
390+ elemName' = elemName (sNameSpaces styleMaps) "w"
399391 let styleEntry = toEntry stylepath epochtime $ renderXml styledoc'
400392
401393 -- construct word/numbering.xml
@@ -472,10 +464,13 @@ writeDocx opts doc@(Pandoc meta _) = do
472464 miscRelEntries ++ otherMediaEntries
473465 return $ fromArchive archive
474466
475−styleToOpenXml :: Style -> [Element]
476−styleToOpenXml style = parStyle : map toStyle alltoktypes
467+styleToOpenXml :: StyleMaps -> Style -> [Element]
468+styleToOpenXml sm style =
469+ maybeToList parStyle ++ mapMaybe toStyle alltoktypes
477470 where alltoktypes = enumFromTo KeywordTok NormalTok
478− toStyle toktype = mknode "w:style" [("w:type","character"),
471+ toStyle toktype | hasStyleName (show toktype) (sCharStyleMap sm) = Nothing
472+ | otherwise = Just $
473+ mknode "w:style" [("w:type","character"),
479474 ("w:customStyle","1"),("w:styleId",show toktype)]
480475 [ mknode "w:name" [("w:val",show toktype)] ()
481476 , mknode "w:basedOn" [("w:val","VerbatimChar")] ()
@@ -496,7 +491,9 @@ styleToOpenXml style = parStyle : map toStyle alltoktypes
496491 tokBg toktype = maybe "auto" (drop 1 . fromColor)
497492 $ (tokenBackground =<< lookup toktype tokStyles)
498493 `mplus` backgroundColor style
499− parStyle = mknode "w:style" [("w:type","paragraph"),
494+ parStyle | hasStyleName "Source Code" (sParaStyleMap sm) = Nothing
495+ | otherwise = Just $
496+ mknode "w:style" [("w:type","paragraph"),
500497 ("w:customStyle","1"),("w:styleId","SourceCode")]
501498 [ mknode "w:name" [("w:val","Source Code")] ()
502499 , mknode "w:basedOn" [("w:val","Normal")] ()
@@ -602,14 +599,14 @@ writeOpenXML opts (Pandoc meta blocks) = do
602599 Just (MetaBlocks [Para xs]) -> xs
603600 Just (MetaInlines xs) -> xs
604601 _ -> []
605− title <- withParaProp (pStyle "Title") $ blocksToOpenXML opts [Para tit | not (null tit)]
606− subtitle <- withParaProp (pStyle "Subtitle") $ blocksToOpenXML opts [Para subtitle' | not (null subtitle')]
607− authors <- withParaProp (pStyle "Author") $ blocksToOpenXML opts $
602+ title <- withParaPropM (pStyleM "Title") $ blocksToOpenXML opts [Para tit | not (null tit)]
603+ subtitle <- withParaPropM (pStyleM "Subtitle") $ blocksToOpenXML opts [Para subtitle' | not (null subtitle')]
604+ authors <- withParaProp (pCustomStyle "Author") $ blocksToOpenXML opts $
608605 map Para auths
609− date <- withParaProp (pStyle "Date") $ blocksToOpenXML opts [Para dat | not (null dat)]
606+ date <- withParaPropM (pStyleM "Date") $ blocksToOpenXML opts [Para dat | not (null dat)]
610607 abstract <- if null abstract'
611608 then return []
612− else withParaProp (pStyle "Abstract") $ blocksToOpenXML opts abstract'
609+ else withParaProp (pCustomStyle "Abstract") $ blocksToOpenXML opts abstract'
613610 let convertSpace (Str x : Space : Str y : xs) = Str (x ++ " " ++ y) : xs
614611 convertSpace (Str x : Str y : xs) = Str (x ++ y) : xs
615612 convertSpace xs = xs
@@ -623,11 +620,23 @@ writeOpenXML opts (Pandoc meta blocks) = do
623620blocksToOpenXML :: WriterOptions -> [Block] -> WS [Element]
624621blocksToOpenXML opts bls = concat `fmap` mapM (blockToOpenXML opts) bls
625622
626−pStyle :: String -> Element
627−pStyle sty = mknode "w:pStyle" [("w:val",sty)] ()
623+pCustomStyle :: String -> Element
624+pCustomStyle sty = mknode "w:pStyle" [("w:val",sty)] ()
628625
629−rStyle :: String -> Element
630−rStyle sty = mknode "w:rStyle" [("w:val",sty)] ()
626+pStyleM :: String -> WS XML.Element
627+pStyleM styleName = do
628+ styleMaps <- gets stStyleMaps
629+ let sty' = getStyleId styleName $ sParaStyleMap styleMaps
630+ return $ mknode "w:pStyle" [("w:val",sty')] ()
631+
632+rCustomStyle :: String -> Element
633+rCustomStyle sty = mknode "w:rStyle" [("w:val",sty)] ()
634+
635+rStyleM :: String -> WS XML.Element
636+rStyleM styleName = do
637+ styleMaps <- gets stStyleMaps
638+ let sty' = getStyleId styleName $ sCharStyleMap styleMaps
639+ return $ mknode "w:rStyle" [("w:val",sty')] ()
631640
632641getUniqueId :: MonadIO m => m String
633642-- the + 20 is to ensure that there are no clashes with the rIds
@@ -641,13 +650,12 @@ blockToOpenXML opts (Div (_,["references"],_) bs) = do
641650 let (hs, bs') = span isHeaderBlock bs
642651 header <- blocksToOpenXML opts hs
643652 -- We put the Bibliography style on paragraphs after the header
644− rest <- withParaProp (pStyle "Bibliography") $ blocksToOpenXML opts bs'
653+ rest <- withParaPropM (pStyleM "Bibliography") $ blocksToOpenXML opts bs'
645654 return (header ++ rest)
646655blockToOpenXML opts (Div _ bs) = blocksToOpenXML opts bs
647656blockToOpenXML opts (Header lev (ident,_,_) lst) = do
648657 setFirstPara
649− headingStyles <- gets stHeadingStyles
650− paraProps <- maybe id (withParaProp . pStyle) (lookup lev headingStyles) $
658+ paraProps <- withParaPropM (pStyleM ("Heading "++show lev)) $
651659 getParaProps False
652660 contents <- inlinesToOpenXML opts lst
653661 usedIdents <- gets stSectionIds
@@ -660,26 +668,27 @@ blockToOpenXML opts (Header lev (ident,_,_) lst) = do
660668 ,("w:name",bookmarkName)] ()
661669 let bookmarkEnd = mknode "w:bookmarkEnd" [("w:id", id')] ()
662670 return [mknode "w:p" [] (paraProps ++ [bookmarkStart, bookmarkEnd] ++ contents)]
663−blockToOpenXML opts (Plain lst) = withParaProp (pStyle "Compact")
671+blockToOpenXML opts (Plain lst) = withParaProp (pCustomStyle "Compact")
664672 $ blockToOpenXML opts (Para lst)
665673-- title beginning with fig: indicates that the image is a figure
666674blockToOpenXML opts (Para [Image alt (src,'f':'i':'g':':':tit)]) = do
667675 setFirstPara
668676 paraProps <- getParaProps False
669677 contents <- inlinesToOpenXML opts [Image alt (src,tit)]
670− captionNode <- withParaProp (pStyle "ImageCaption")
678+ captionNode <- withParaProp (pCustomStyle "ImageCaption")
671679 $ blockToOpenXML opts (Para alt)
672680 return $ mknode "w:p" [] (paraProps ++ contents) : captionNode
673681-- fixDisplayMath sometimes produces a Para [] as artifact
674682blockToOpenXML _ (Para []) = return []
675683blockToOpenXML opts (Para lst) = do
676− isFirstPara <- gets stFirstPara
684+ isFirstPara <- gets stFirstPara
677685 paraProps <- getParaProps $ case lst of
678686 [Math DisplayMath _] -> True
679687 _ -> False
688+ bodyTextStyle <- pStyleM "Body Text"
680689 let paraProps' = case paraProps of
681− [] | isFirstPara -> [mknode "w:pPr" [] [(pStyle "FirstParagraph")]]
682− [] -> [mknode "w:pPr" [] [(pStyle "BodyText")]]
690+ [] | isFirstPara -> [mknode "w:pPr" [] [pCustomStyle "FirstParagraph"]]
691+ [] -> [mknode "w:pPr" [] [bodyTextStyle]]
683692 ps -> ps
684693 modify $ \s -> s { stFirstPara = False }
685694 contents <- inlinesToOpenXML opts lst
@@ -688,11 +697,11 @@ blockToOpenXML _ (RawBlock format str)
688697 | format == Format "openxml" = return [ x | Elem x <- parseXML str ]
689698 | otherwise = return []
690699blockToOpenXML opts (BlockQuote blocks) = do
691− p <- withParaProp (pStyle "BlockQuote") $ blocksToOpenXML opts blocks
700+ p <- withParaPropM (pStyleM "Block Text") $ blocksToOpenXML opts blocks
692701 setFirstPara
693702 return p
694703blockToOpenXML opts (CodeBlock attrs str) = do
695− p <- withParaProp (pStyle "SourceCode") $ (blockToOpenXML opts $ Para [Code attrs str])
704+ p <- withParaProp (pCustomStyle "SourceCode") (blockToOpenXML opts $ Para [Code attrs str])
696705 setFirstPara
697706 return p
698707blockToOpenXML _ HorizontalRule = do
@@ -707,7 +716,7 @@ blockToOpenXML opts (Table caption aligns widths headers rows) = do
707716 let captionStr = stringify caption
708717 caption' <- if null caption
709718 then return []
710− else withParaProp (pStyle "TableCaption")
719+ else withParaProp (pCustomStyle "TableCaption")
711720 $ blockToOpenXML opts (Para caption)
712721 let alignmentFor al = mknode "w:jc" [("w:val",alignmentToString al)] ()
713722 let cellToOpenXML (al, cell) = withParaProp (alignmentFor al)
@@ -718,32 +727,36 @@ blockToOpenXML opts (Table caption aligns widths headers rows) = do
718727 [ mknode "w:tcBorders" []
719728 $ mknode "w:bottom" [("w:val","single")] ()
720729 , mknode "w:vAlign" [("w:val","bottom")] () ]
721− let emptyCell = [mknode "w:p" [] [mknode "w:pPr" []
722− [mknode "w:pStyle" [("w:val","Compact")] ()]]]
730+ let emptyCell = [mknode "w:p" [] [pCustomStyle "Compact"]]
723731 let mkcell border contents = mknode "w:tc" []
724732 $ [ borderProps | border ] ++
725733 if null contents
726734 then emptyCell
727735 else contents
728− let mkrow border cells = mknode "w:tr" [] $ map (mkcell border) cells
736+ let mkrow border cells = mknode "w:tr" [] $
737+ [mknode "w:trPr" [] [
738+ mknode "w:cnfStyle" [("w:firstRow","1")] ()] | border]
739+ ++ map (mkcell border) cells
729740 let textwidth = 7920 -- 5.5 in in twips, 1/20 pt
730741 let fullrow = 5000 -- 100% specified in pct
731742 let rowwidth = fullrow * sum widths
732743 let mkgridcol w = mknode "w:gridCol"
733744 [("w:w", show (floor (textwidth * w) :: Integer))] ()
745+ let hasHeader = not (all null headers)
734746 return $
735747 caption' ++
736748 [mknode "w:tbl" []
737749 ( mknode "w:tblPr" []
738750 ( mknode "w:tblStyle" [("w:val","TableNormal")] () :
739751 mknode "w:tblW" [("w:type", "pct"), ("w:w", show rowwidth)] () :
752+ mknode "w:tblLook" [("w:firstRow","1") | hasHeader ] () :
740753 [ mknode "w:tblCaption" [("w:val", captionStr)] ()
741754 | not (null caption) ] )
742755 : mknode "w:tblGrid" []
743756 (if all (==0) widths
744757 then []
745758 else map mkgridcol widths)
746− : [ mkrow True headers' | not (all null headers) ] ++
759+ : [ mkrow True headers' | hasHeader ] ++
747760 map (mkrow False) rows'
748761 )]
749762blockToOpenXML opts (BulletList lst) = do
@@ -767,9 +780,9 @@ blockToOpenXML opts (DefinitionList items) = do
767780
768781definitionListItemToOpenXML :: WriterOptions -> ([Inline],[[Block]]) -> WS [Element]
769782definitionListItemToOpenXML opts (term,defs) = do
770− term' <- withParaProp (pStyle "DefinitionTerm")
783+ term' <- withParaProp (pCustomStyle "DefinitionTerm")
771784 $ blockToOpenXML opts (Para term)
772− defs' <- withParaProp (pStyle "Definition")
785+ defs' <- withParaProp (pCustomStyle "Definition")
773786 $ concat `fmap` mapM (blocksToOpenXML opts) defs
774787 return $ term' ++ defs'
775788
@@ -833,6 +846,9 @@ withTextProp d p = do
833846 popTextProp
834847 return res
835848
849+withTextPropM :: WS Element -> WS a -> WS a
850+withTextPropM = (. flip withTextProp) . (>>=)
851+
836852getParaProps :: Bool -> WS [Element]
837853getParaProps displayMathPara = do
838854 props <- gets stParaProperties
@@ -861,6 +877,9 @@ withParaProp d p = do
861877 popParaProp
862878 return res
863879
880+withParaPropM :: WS Element -> WS a -> WS a
881+withParaPropM = (. flip withParaProp) . (>>=)
882+
864883formattedString :: String -> WS [Element]
865884formattedString str = do
866885 props <- getTextProps
@@ -943,25 +962,26 @@ inlineToOpenXML opts (Math mathType str) = do
943962 Right r -> return [r]
944963 Left _ -> inlinesToOpenXML opts (texMathToInlines mathType str)
945964inlineToOpenXML opts (Cite _ lst) = inlinesToOpenXML opts lst
946−inlineToOpenXML opts (Code attrs str) =
947− withTextProp (rStyle "VerbatimChar")
948− $ if writerHighlight opts
949− then case highlight formatOpenXML attrs str of
950− Nothing -> unhighlighted
951− Just h -> return h
952− else unhighlighted
953− where unhighlighted = intercalate [br] `fmap`
954− (mapM formattedString $ lines str)
955− formatOpenXML _fmtOpts = intercalate [br] . map (map toHlTok)
956− toHlTok (toktype,tok) = mknode "w:r" []
957− [ mknode "w:rPr" []
958− [ rStyle $ show toktype ]
959− , mknode "w:t" [("xml:space","preserve")] tok ]
965+inlineToOpenXML opts (Code attrs str) = do
966+ let unhighlighted = intercalate [br] `fmap`
967+ (mapM formattedString $ lines str)
968+ formatOpenXML _fmtOpts = intercalate [br] . map (map toHlTok)
969+ toHlTok (toktype,tok) = mknode "w:r" []
970+ [ mknode "w:rPr" []
971+ [ rCustomStyle (show toktype) ]
972+ , mknode "w:t" [("xml:space","preserve")] tok ]
973+ withTextProp (rCustomStyle "VerbatimChar")
974+ $ if writerHighlight opts
975+ then case highlight formatOpenXML attrs str of
976+ Nothing -> unhighlighted
977+ Just h -> return h
978+ else unhighlighted
960979inlineToOpenXML opts (Note bs) = do
961980 notes <- gets stFootnotes
962981 notenum <- getUniqueId
982+ footnoteStyle <- rStyleM "Footnote Reference"
963983 let notemarker = mknode "w:r" []
964− [ mknode "w:rPr" [] (rStyle "FootnoteRef")
984+ [ mknode "w:rPr" [] footnoteStyle
965985 , mknode "w:footnoteRef" [] () ]
966986 let notemarkerXml = RawInline (Format "openxml") $ ppElement notemarker
967987 let insertNoteRef (Plain ils : xs) = Plain (notemarkerXml : ils) : xs
@@ -971,22 +991,22 @@ inlineToOpenXML opts (Note bs) = do
971991 oldParaProperties <- gets stParaProperties
972992 oldTextProperties <- gets stTextProperties
973993 modify $ \st -> st{ stListLevel = -1, stParaProperties = [], stTextProperties = [] }
974− contents <- withParaProp (pStyle "FootnoteText") $ blocksToOpenXML opts
994+ contents <- withParaPropM (pStyleM "Footnote Text") $ blocksToOpenXML opts
975995 $ insertNoteRef bs
976996 modify $ \st -> st{ stListLevel = oldListLevel, stParaProperties = oldParaProperties,
977997 stTextProperties = oldTextProperties }
978998 let newnote = mknode "w:footnote" [("w:id", notenum)] $ contents
979999 modify $ \s -> s{ stFootnotes = newnote : notes }
9801000 return [ mknode "w:r" []
981− [ mknode "w:rPr" [] (rStyle "FootnoteRef")
1001+ [ mknode "w:rPr" [] footnoteStyle
9821002 , mknode "w:footnoteReference" [("w:id", notenum)] () ] ]
9831003-- internal link:
9841004inlineToOpenXML opts (Link txt ('#':xs,_)) = do
985− contents <- withTextProp (rStyle "Link") $ inlinesToOpenXML opts txt
1005+ contents <- withTextPropM (rStyleM "Hyperlink") $ inlinesToOpenXML opts txt
9861006 return [ mknode "w:hyperlink" [("w:anchor",xs)] contents ]
9871007-- external link:
9881008inlineToOpenXML opts (Link txt (src,_)) = do
989− contents <- withTextProp (rStyle "Link") $ inlinesToOpenXML opts txt
1009+ contents <- withTextPropM (rStyleM "Hyperlink") $ inlinesToOpenXML opts txt
9901010 extlinks <- gets stExternalLinks
9911011 id' <- case M.lookup src extlinks of
9921012 Just i -> return i
@@ -1088,7 +1108,7 @@ defaultFootnotes = [ mknode "w:footnote"
10881108 [ mknode "w:p" [] $
10891109 [ mknode "w:r" [] $
10901110 [ mknode "w:continuationSeparator" [] ()]]]]
1091−
1111+
10921112parseXml :: Archive -> Archive -> String -> IO Element
10931113parseXml refArchive distArchive relpath =
10941114 case ((findEntryByPath relpath refArchive `mplus`
tests/Tests/Writers/Docx.hsnew file+128
@@ -0,0 +1,128 @@
1+module Tests.Writers.Docx (tests) where
2+
3+import Text.Pandoc.Options
4+import Text.Pandoc.Readers.Native
5+import Text.Pandoc.Definition
6+import Tests.Helpers
7+import Test.Framework
8+import Text.Pandoc.Readers.Docx
9+import Text.Pandoc.Writers.Docx
10+
11+type Options = (WriterOptions, ReaderOptions)
12+
13+compareOutput :: Options
14+ -> FilePath
15+ -> IO (Pandoc, Pandoc)
16+compareOutput opts nativeFile = do
17+ nf <- Prelude.readFile nativeFile
18+ df <- writeDocx (fst opts) (readNative nf)
19+ let (p, _) = readDocx (snd opts) df
20+ return (p, readNative nf)
21+
22+testCompareWithOptsIO :: Options -> String -> FilePath -> IO Test
23+testCompareWithOptsIO opts name nativeFile = do
24+ (dp, np) <- compareOutput opts nativeFile
25+ return $ test id name (dp, np)
26+
27+testCompareWithOpts :: Options -> String -> FilePath -> Test
28+testCompareWithOpts opts name nativeFile =
29+ buildTest $ testCompareWithOptsIO opts name nativeFile
30+
31+testCompare :: String -> FilePath -> Test
32+testCompare = testCompareWithOpts def
33+
34+tests :: [Test]
35+tests = [ testGroup "inlines"
36+ [ testCompare
37+ "font formatting"
38+ "docx/inline_formatting_writer.native"
39+ , testCompare
40+ "font formatting with character styles"
41+ "docx/char_styles.native"
42+ , testCompare
43+ "hyperlinks"
44+ "docx/links_writer.native"
45+ , testCompare
46+ "inline image"
47+ "docx/image_no_embed_writer.native"
48+ , testCompare
49+ "inline image in links"
50+ "docx/inline_images_writer.native"
51+ , testCompare
52+ "handling unicode input"
53+ "docx/unicode.native"
54+ , testCompare
55+ "literal tabs"
56+ "docx/tabs.native"
57+ , testCompare
58+ "normalizing inlines"
59+ "docx/normalize.native"
60+ , testCompare
61+ "normalizing inlines deep inside blocks"
62+ "docx/deep_normalize.native"
63+ , testCompare
64+ "move trailing spaces outside of formatting"
65+ "docx/trailing_spaces_in_formatting.native"
66+ , testCompare
67+ "inline code (with VerbatimChar style)"
68+ "docx/inline_code.native"
69+ , testCompare
70+ "inline code in subscript and superscript"
71+ "docx/verbatim_subsuper.native"
72+ ]
73+ , testGroup "blocks"
74+ [ testCompare
75+ "headers"
76+ "docx/headers.native"
77+ , testCompare
78+ "headers already having auto identifiers"
79+ "docx/already_auto_ident.native"
80+ , testCompare
81+ "numbered headers automatically made into list"
82+ "docx/numbered_header.native"
83+ , testCompare
84+ "i18n blocks (headers and blockquotes)"
85+ "docx/i18n_blocks.native"
86+ -- Continuation does not survive round-trip
87+ , testCompare
88+ "lists"
89+ "docx/lists_writer.native"
90+ , testCompare
91+ "definition lists"
92+ "docx/definition_list.native"
93+ , testCompare
94+ "custom defined lists in styles"
95+ "docx/german_styled_lists.native"
96+ , testCompare
97+ "footnotes and endnotes"
98+ "docx/notes.native"
99+ , testCompare
100+ "blockquotes (parsing indent as blockquote)"
101+ "docx/block_quotes_parse_indent.native"
102+ , testCompare
103+ "hanging indents"
104+ "docx/hanging_indent.native"
105+ -- tables headers do not survive round-trip, should look into that
106+ , testCompare
107+ "tables"
108+ "docx/tables.native"
109+ , testCompare
110+ "tables with lists in cells"
111+ "docx/table_with_list_cell.native"
112+ , testCompare
113+ "code block"
114+ "docx/codeblock.native"
115+ , testCompare
116+ "dropcap paragraphs"
117+ "docx/drop_cap.native"
118+ ]
119+ , testGroup "metadata"
120+ [ testCompareWithOpts (def,def{readerStandalone=True})
121+ "metadata fields"
122+ "docx/metadata.native"
123+ , testCompareWithOpts (def,def{readerStandalone=True})
124+ "stop recording metadata with normal text"
125+ "docx/metadata_after_normal.native"
126+ ]
127+
128+ ]
tests/docx/image_no_embed_writer.nativenew file+2
@@ -0,0 +1,2 @@
1+[Para [Str "An",Space,Str "image:"]
2+,Para [Image [] ("media/rId25.jpg","")]]
tests/docx/inline_formatting_writer.nativenew file+5
@@ -0,0 +1,5 @@
1+[Para [Str "Regular",Space,Str "text",Space,Emph [Str "italics"],Space,Strong [Str "bold",Space,Emph [Str "bold",Space,Str "italics"]],Str "."]
2+,Para [Str "This",Space,Str "is",Space,SmallCaps [Str "Small",Space,Str "Caps"],Str ",",Space,Str "and",Space,Str "this",Space,Str "is",Space,Strikeout [Str "strikethrough"],Str "."]
3+,Para [Str "Some",Space,Str "people",Space,Str "use",Space,Emph [Str "single",Space,Str "underlines",Space,Str "for",Space,Str "emphasis"],Str "."]
4+,Para [Str "Above",Space,Str "the",Space,Str "line",Space,Str "is",Space,Superscript [Str "superscript"],Space,Str "and",Space,Str "below",Space,Str "the",Space,Str "line",Space,Str "is",Space,Subscript [Str "subscript"],Str "."]
5+,Para [Str "A",Space,Str "line",LineBreak,Str "break."]]
tests/docx/inline_images_writer.nativenew file+2
@@ -0,0 +1,2 @@
1+[Para [Str "This",Space,Str "picture",Space,Image [] ("media/rId26.jpg",""),Space,Str "is",Space,Str "an",Space,Str "identicon."]
2+,Para [Str "Here",Space,Str "is",Space,Link [Str "one",Space,Image [] ("media/rId27.jpg",""),Space,Str "that"] ("http://www.google.com",""),Space,Str "links."]]
tests/docx/links_writer.nativenew file+6
@@ -0,0 +1,6 @@
1+[Header 2 ("an-internal-link-and-an-external-link",[],[]) [Str "An",Space,Str "internal",Space,Str "link",Space,Str "and",Space,Str "an",Space,Str "external",Space,Str "link"]
2+,Para [Str "An",Space,Link [Str "external",Space,Str "link"] ("http://google.com",""),Space,Str "to",Space,Str "a",Space,Str "popular",Space,Str "website."]
3+,Para [Str "An",Space,Link [Str "external",Space,Str "link"] ("http://johnmacfarlane.net/pandoc/README.html#synopsis",""),Space,Str "to",Space,Str "a",Space,Str "website",Space,Str "with",Space,Str "an",Space,Str "anchor."]
4+,Para [Str "An",Space,Link [Str "internal",Space,Str "link"] ("#a-section-for-testing-link-targets",""),Space,Str "to",Space,Str "a",Space,Str "section",Space,Str "header."]
5+,Para [Str "An",Space,Link [Str "internal",Space,Str "link"] ("#my_bookmark",""),Space,Str "to",Space,Str "a",Space,Str "bookmark."]
6+,Header 2 ("a-section-for-testing-link-targets",[],[]) [Str "A",Space,Str "section",Space,Str "for",Space,Str "testing",Space,Str "link",Space,Str "targets"]]
tests/docx/lists_writer.nativenew file+17
@@ -0,0 +1,17 @@
1+[Header 2 ("some-nested-lists",[],[]) [Str "Some",Space,Str "nested",Space,Str "lists"]
2+,OrderedList (1,Decimal,Period)
3+ [[Para [Str "one"]]
4+ ,[Para [Str "two"]
5+ ,OrderedList (1,LowerAlpha,DefaultDelim)
6+ [[Para [Str "a"]]
7+ ,[Para [Str "b"]]]]]
8+,BulletList
9+ [[Para [Str "one"]]
10+ ,[Para [Str "two"]
11+ ,BulletList
12+ [[Para [Str "three"]
13+ ,BulletList
14+ [[Para [Str "four"]]]]]]
15+ ,[Para [Str "Same",Space,Str "list"]]]
16+,BulletList
17+ [[Para [Str "Different",Space,Str "list",Space,Str "adjacent",Space,Str "to",Space,Str "the",Space,Str "one",Space,Str "above."]]]]
tests/media/rId25.jpgnew file

No changes to the content.

tests/media/rId26.jpgnew file

No changes to the content.

tests/media/rId27.jpgnew file

No changes to the content.

tests/test-pandoc.hs+2
@@ -20,6 +20,7 @@ import qualified Tests.Writers.Native
2020import qualified Tests.Writers.Markdown
2121import qualified Tests.Writers.Plain
2222import qualified Tests.Writers.AsciiDoc
23+import qualified Tests.Writers.Docx
2324import qualified Tests.Shared
2425import qualified Tests.Walk
2526import Text.Pandoc.Shared (inDirectory)
@@ -38,6 +39,7 @@ tests = [ testGroup "Old" Tests.Old.tests
3839 , testGroup "Markdown" Tests.Writers.Markdown.tests
3940 , testGroup "Plain" Tests.Writers.Plain.tests
4041 , testGroup "AsciiDoc" Tests.Writers.AsciiDoc.tests
42+ , testGroup "Docx" Tests.Writers.Docx.tests
4143 ]
4244 , testGroup "Readers"
4345 [ testGroup "LaTeX" Tests.Readers.LaTeX.tests